Codebase list python-faraday / 45b3225
Merge tag 'upstream/1.0.16' into kali/master Upstream version 1.0.16 Sophie Brun 8 years ago
103 changed file(s) with 52003 addition(s) and 14637 deletion(s). Raw diff Collapse all Expand all
1414 * Andrés López Luksenberg
1515 * Juan Urbano
1616 * Elian Gidoni
17 * Andres Tarantini
18 * Ezequiel Tavella
19 * Martin Tartarelli
99 New features in the latest update
1010 =====================================
1111
12 TBA:
12 Dec 18, 2015:
13 ---
14 * Immunity Canvas plugin added
15 * Added Dig plugin
16 * Added Traceroute plugin
17 * Fixed bug in first run of Faraday with log path and API errors
18 * Added parametrization for port configuration on APIs
19 * Refactor Plugin Base to update active WS name in var
20 * Refactor Plugins to use current WS in temp filename under $HOME/.faraday/data. Affected Plugins:
21 - amap
22 - dnsmap
23 - nmap
24 - sslcheck
25 - wcscan
26 - webfuzzer
27 - nikto
28 * Fixed bug get_installed_distributions from handler exceptions
29 * Added Wiki information about running Faraday without configuring CouchDB
30 * Fixed Unicode bug in Nexpose-full Plugin
31 * Filter false-positives in Status Report
32 * Fixed bug that prevented the use of "reports" and "cwe" strings in Workspace names
33 * Added port to Service type target in new vuln modal
34 * Added new scripts for faraday plugin:
35 - /bin/delAllVulnsWith.py - delete all vulns that match a regex
36 - /bin/getAllbySrv.py - get all IP addresses that have defined open port
37 - /bin/getAllIpsNotServices.py added - get all IPs from targets without services
38 * Fixed bug null last workspace
39 * Fixed bugs in CSV export/import in QT
40
41 Oct 2, 2015:
1342 ---
1443 * Continuous Scanning Tool cscan added to ./scripts/cscan
1544 * Fix for saving objects without parent
0 1.0.15
0 1.0.16
4141 _http_server.stop()
4242
4343
44 def startAPIs(plugin_manager, model_controller, mapper_manager, hostname=None, port=None):
44 def startAPIs(plugin_manager, model_controller, mapper_manager, hostname, port):
4545 global _rest_controllers
4646 global _http_server
4747 _rest_controllers = [PluginControllerAPI(plugin_manager, mapper_manager), ModelControllerAPI(model_controller)]
4848
49 #TODO: some way to get defaults.. from config?
50 if str(hostname) == "None":
51 hostname = "localhost"
52 if str(port) == "None":
53 port = 9977
54
55 if CONF.getApiRestfulConInfo() is None:
56 CONF.setApiRestfulConInfo(hostname, port)
57
5849 app = Flask('APISController')
5950
6051 _http_server = HTTPServer(WSGIContainer(app))
61 _http_server.listen(port,address=hostname)
52 _http_server.listen(port, address=hostname)
6253
6354 routes = [r for c in _rest_controllers for r in c.getRoutes()]
6455
65 for route in routes:
66 app.add_url_rule(route.path, view_func=route.view_func, methods=route.methods)
56 for route in routes:
57 app.add_url_rule(route.path, view_func=route.view_func, methods=route.methods)
6758
6859 logging.getLogger("tornado.access").addHandler(logger.getLogger(app))
6960 logging.getLogger("tornado.access").propagate = False
168159 hostid = json_data['hostid']
169160
170161 host = self.controller.getHost(hostid)
171 if not host:
172 return self.badRequest("no plugin available for cmd")
162 if not host:
163 return self.badRequest("no plugin available for cmd")
173164
174165 visitor = VulnsLookupVisitor(vulnid)
175166 host.accept(visitor)
177168 if not visitor.vulns:
178169 return self.noContent('No vuls matched criteria')
179170
180 # forward to controller
171 # forward to controller
181172 for vuln, parents in zip(visitor.vulns, visitor.parents):
182173 last_parent = parents[0]
183174 self.controller.delVulnSYNC(last_parent, vuln.getID())
197188 hostid = json_data['hostid']
198189
199190 host = self.controller.getHost(hostid)
200 if not host:
201 return self.badRequest("no plugin available for cmd")
191 if not host:
192 return self.badRequest("no plugin available for cmd")
202193
203194 visitor = VulnsLookupVisitor(vulnid)
204195 host.accept(visitor)
212203 resolution = json_data.get('resolution', None)
213204 refs = json_data.get('refs', None)
214205
215 # forward to controller
216 for vuln in visitor.vulns:
217 self.controller.editVulnSYNC(vuln, name, desc, severity, resolution, refs)
206 # forward to controller
207 for vuln in visitor.vulns:
208 self.controller.editVulnSYNC(vuln, name, desc, severity, resolution, refs)
218209
219210 return self.ok("output successfully sent to plugin")
220211
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import auth.users
9
10 RolesAdmited = set(["local_admin"])
11
12
13
14 class Roles(object):
15 """Roles for a user are defined here"""
16 def __init__(self, *args):
17 self.roles = []
18 if set(args).issubset(RolesAdmited):
19 self.roles.extend(args)
20
21 def __iter__(self):
22 return iter(self.roles)
23
24 class codes:
25 """
26 an enumeration with different result/status codes
27 """
28 successfulLogin = 0
29 badUserOrPassword = 1
30
31 __descriptions = {
32 successfulLogin :"Successful Logon",
33 badUserOrPassword : "Bad username or password",
34 }
35
36 @staticmethod
37 def getDescription(code):
38 """
39 Returns the description for a given code
40 """
41 return codes.__descriptions.get(code,"code does not exist")
42
43
44 class SecurityManager(object):
45 """
46 Handles the security and authentication in the system.
47 it exposes some methods used to authenticate users and check permissions
48 over different objects on the model.
49 """
50
51
52
53
54
55
56
57
58 def __init__(self):
59 self.current_user = None
60
61 def authenticateUser(self, username, password):
62 """
63 Authenticates a user.
64 It returns 2 values:
65 First value:
66 if username and password are correct it returns a User object.
67 It returns None if authentication fails
68 Second value:
69 returns a result code
70 This code can be used later to get a description of the situation.
71 To get the description use
72 """
73
74
75
76
77
78
79 user = auth.users.User(username,password)
80 self.current_user = user
81
82 return codes.successfulLogin
83
84 def getCurrentUser(self):
85 return self.current_user
86
87 def getUserRoles(self):
88 return Roles("local_admin")
89
90 def checkPermissions(self, operation):
91 providers = self.getProviders(operation)
92 if any( [ prov.isAllowed(securityManager = self,
93 aUser = self.getCurrentUser(),
94 anOperation = operation)
95 for prov in providers ] ):
96 return True
97 raise SecurityFailException("No permission for anything")
98
99 def getProviders(self, operation):
100 return [prov() for prov in SecurityProvider.__subclasses__() if prov.handlesOp(operation) ]
101
102
103 class SecurityProvider(object):
104 def isAllowed(self, securityManager, aUser, anOperation):
105 raise NotImplementedError("Should not implement abstract")
106
107 class WorkspacePermisionProvider(SecurityProvider):
108 handles = ["syncActiveWorkspace"]
109 def __init__(self):
110 self.ops_per_roles = {'syncActiveWorkspace' : Roles('pentester', 'admin').roles }
111
112 @staticmethod
113 def handlesOp(anOperation):
114 return anOperation in WorkspacePermisionProvider.handles
115
116 def isAllowed(self, securityManager, aUser, anOperation):
117 """ Checks if the user has the role needed to run the operation """
118 allowd = any(map(lambda x: x in self.ops_per_roles[anOperation], securityManager.getUserRoles()))
119
120 return allowd
121
122
123 class SecurityFailException(Exception):
124 pass
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import auth.users
9
10 RolesAdmited = set(["local_admin"])
11
12
13
14 class Roles(object):
15 """Roles for a user are defined here"""
16 def __init__(self, *args):
17 self.roles = []
18 if set(args).issubset(RolesAdmited):
19 self.roles.extend(args)
20
21 def __iter__(self):
22 return iter(self.roles)
23
24 class codes:
25 """
26 an enumeration with different result/status codes
27 """
28 successfulLogin = 0
29 badUserOrPassword = 1
30
31 __descriptions = {
32 successfulLogin :"Successful Logon",
33 badUserOrPassword : "Bad username or password",
34 }
35
36 @staticmethod
37 def getDescription(code):
38 """
39 Returns the description for a given code
40 """
41 return codes.__descriptions.get(code,"code does not exist")
42
43
44 class SecurityManager(object):
45 """
46 Handles the security and authentication in the system.
47 it exposes some methods used to authenticate users and check permissions
48 over different objects on the model.
49 """
50
51
52
53
54
55
56
57
58 def __init__(self):
59 self.current_user = None
60
61 def authenticateUser(self, username, password):
62 """
63 Authenticates a user.
64 It returns 2 values:
65 First value:
66 if username and password are correct it returns a User object.
67 It returns None if authentication fails
68 Second value:
69 returns a result code
70 This code can be used later to get a description of the situation.
71 To get the description use
72 """
73
74
75
76
77
78
79 user = auth.users.User(username,password)
80 self.current_user = user
81
82 return codes.successfulLogin
83
84 def getCurrentUser(self):
85 return self.current_user
86
87 def getUserRoles(self):
88 return Roles("local_admin")
89
90 def checkPermissions(self, operation):
91 providers = self.getProviders(operation)
92 if any( [ prov.isAllowed(securityManager = self,
93 aUser = self.getCurrentUser(),
94 anOperation = operation)
95 for prov in providers ] ):
96 return True
97 raise SecurityFailException("No permission for anything")
98
99 def getProviders(self, operation):
100 return [prov() for prov in SecurityProvider.__subclasses__() if prov.handlesOp(operation) ]
101
102
103 class SecurityProvider(object):
104 def isAllowed(self, securityManager, aUser, anOperation):
105 raise NotImplementedError("Should not implement abstract")
106
107 class WorkspacePermisionProvider(SecurityProvider):
108 handles = ["syncActiveWorkspace"]
109 def __init__(self):
110 self.ops_per_roles = {'syncActiveWorkspace' : Roles('pentester', 'admin').roles }
111
112 @staticmethod
113 def handlesOp(anOperation):
114 return anOperation in WorkspacePermisionProvider.handles
115
116 def isAllowed(self, securityManager, aUser, anOperation):
117 """ Checks if the user has the role needed to run the operation """
118 allowd = any(map(lambda x: x in self.ops_per_roles[anOperation], securityManager.getUserRoles()))
119
120 return allowd
121
122
123 class SecurityFailException(Exception):
124 pass
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7 import os
8 import sys
9 import uuid
10
11
12
13 class User(object):
14 """
15 This represents a user on the system.
16 Users are assigned to work on different workspaces and
17 some permissions are configured.
18 A user can be part of groups.
19 """
20 def __init__(self, name, passwd = "", display_name = "", groups = [], level = 0):
21
22 self.name = name
23 self.__id = uuid.uuid4()
24
25 self.display_name = display_name or name
26 self._groups = []
27 self._level = level
28
29
30 self.__password = passwd
31
32
33
34 self.lastlogon = None
35
36
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7 import os
8 import sys
9 import uuid
10
11
12
13 class User(object):
14 """
15 This represents a user on the system.
16 Users are assigned to work on different workspaces and
17 some permissions are configured.
18 A user can be part of groups.
19 """
20 def __init__(self, name, passwd = "", display_name = "", groups = [], level = 0):
21
22 self.name = name
23 self.__id = uuid.uuid4()
24
25 self.display_name = display_name or name
26 self._groups = []
27 self._level = level
28
29
30 self.__password = passwd
31
32
33
34 self.lastlogon = None
35
36
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2
3 '''
4 Faraday Penetration Test IDE
5 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
6 See the file 'doc/LICENSE' for the license information
7
8 '''
9
10 import re
11 regex="ssl\-cert|ssl\-date|Traceroute Information|TCP\/IP Timestamps Supported|OS Identification|Common Platform Enumeration"
12 c=0
13 for host in api.__model_controller.getAllHosts():
14 hostnames=""
15 for v in host.getVulns():
16 if re.match(regex,v.name) is not None:
17 api.delVulnFromHost(v.id,host.id)
18 c+=1
19
20 for i in host.getAllInterfaces():
21 for s in i.getAllServices():
22 for v in s.getVulns():
23 if re.match(regex,v.name) is not None:
24 api.delVulnFromService(v.id,host.id,s.id)
25 c+=1
26
27 print "Vulnerabilities deleted %s" % c
1616 if __name__ == '__main__':
1717 parser = argparse.ArgumentParser()
1818
19 # executes received code
1920 parser.add_argument('-e')
21 # where to store fplugin's output
2022 parser.add_argument('-o')
23 # file with code to execute
2124 parser.add_argument('-f')
2225
2326 #get all hosts
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2
3 '''
4 Faraday Penetration Test IDE
5 Copyright (C) 2015 Infobyte LLC (http://www.infobytesec.com/)
6 See the file 'doc/LICENSE' for the license information
7
8 '''
9
10 # Get All IPs from targets without services
11 for host in api.__model_controller.getAllHosts():
12
13 for i in host.getAllInterfaces():
14 if not i.getAllServices():
15 print host.name
16
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2
3 '''
4 Faraday Penetration Test IDE
5 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
6 See the file 'doc/LICENSE' for the license information
7
8 '''
9 port='8080'
10 webs={}
11 for host in api.__model_controller.getAllHosts():
12
13 for i in host.getAllInterfaces():
14 for s in i.getAllServices():
15 for p in s.getPorts():
16 if str(p) == port:
17 webs[host.name]=1
18
19 for k,v in webs.iteritems():
20 print k
279279 def getTktTemplate(self):
280280 return self._tkt_template
281281
282
283
284282 def setLastWorkspace(self, workspaceName):
285283 self._last_workspace = workspaceName
286284
410408
411409 ROOT = Element("faraday")
412410
411 tree = self._getTree()
412
413413 API_CON_INFO_HOST = Element(CONST_API_CON_INFO_HOST)
414 API_CON_INFO_HOST.text = self.getApiConInfoHost()
414 API_CON_INFO_HOST.text = self._getValue(tree, CONST_API_CON_INFO_HOST)
415 # API_CON_INFO_HOST.text = self.getApiConInfoHost()
415416 ROOT.append(API_CON_INFO_HOST)
416417
417418 API_CON_INFO_PORT = Element(CONST_API_CON_INFO_PORT)
418 API_CON_INFO_PORT.text = str(self.getApiConInfoPort())
419 API_CON_INFO_PORT.text = self._getValue(tree, CONST_API_CON_INFO_PORT)
420 # API_CON_INFO_PORT.text = str(self.getApiConInfoPort())
419421 ROOT.append(API_CON_INFO_PORT)
420422
421423 API_RESTFUL_CON_INFO_PORT = Element(CONST_API_RESTFUL_CON_INFO_PORT)
422 API_RESTFUL_CON_INFO_PORT.text = str(self.getApiRestfulConInfoPort())
424 API_RESTFUL_CON_INFO_PORT.text = self._getValue(tree, CONST_API_RESTFUL_CON_INFO_PORT)
425 # API_RESTFUL_CON_INFO_PORT.text = str(self.getApiRestfulConInfoPort())
423426 ROOT.append(API_RESTFUL_CON_INFO_PORT)
424427
425428 APPNAME = Element(CONST_APPNAME)
11 <faraday>
22
33 <appname>Faraday - Penetration Test IDE</appname>
4 <version>1.0.15</version>
4 <version>1.0.16</version>
55 <debug_status>0</debug_status>
66 <font>-Misc-Fixed-medium-r-normal-*-12-100-100-100-c-70-iso8859-1</font>
77 <home_path>~/</home_path>
1515 self.path = path
1616 self.modelobjects = modelobjects
1717
18 #(self,ip,port,protocol,name,desc,severity,type):
1819 def getVulnCSVField(self, host, vuln, serv = None):
1920 vdate = str(date.fromtimestamp(vuln.getMetadata().create_time))
2021 status = 'vuln'
21 level = vuln.severity
22 level = str(vuln.severity)
2223 name = vuln.name
24 vtype = '1'
2325 target = host.name + ( ':' + ','.join([ str(x) for x in serv.getPorts()]) if serv else '')
24 if vuln.class_signature == "VulnerabilityWeb":
25 target = "%s/%s" % (target, vuln.path)
26 if vuln.class_signature == "VulnerabilityWeb":
27 vtype = '2'
28 desc = vuln.desc.replace('\n', '<br/>') if vuln.desc else ""
29 desc = desc.replace(',', '&nbsp;')
30
31 csv_fields = [ vdate , host.name ,' '.join([ str(x) for x in serv.getPorts()]) if serv else '', serv.getProtocol() if serv else "", name, desc, level , vtype]
2632
27 desc = vuln.desc.replace('\n', '<br/>')
28
29 csv_fields = [ vdate , status , str(level) , name , target , desc]
33
3034 try:
3135 encoded_csv_fields = map(lambda x: x.encode('utf-8'), csv_fields)
32 except Exception as e:
36 except Exception as e:
3337 print e
3438
3539 field = "|".join(encoded_csv_fields)
66 ###
77
88 #ZDOTDIR="~/.faraday/zsh/" /bin/zsh
9 FARADAY_ZSH_RPORT="9977"
10 FARADAY_ZSH_HOST="127.0.0.1"
11 if [ $# -eq 2 ]; then
12 FARADAY_ZSH_HOST=$1
13 FARADAY_ZSH_RPORT=$2
14 else
15 if [ $# -gt 2 ] || [ $# -eq 1 ]; then
16 echo "[*] Usage $0 host port"
17 echo "[*] Usage $0 127.0.0.1 9977"
18 exit
19 else
20 echo "[!] Using default configuration" $FARADAY_ZSH_HOST:$FARADAY_ZSH_RPORT
21 fi
22 fi
23
24 export FARADAY_ZSH_RPORT
25 export FARADAY_ZSH_HOST
926 FARADAYZDOTDIR="$HOME/.faraday/zsh/"
1027 OLDZDOTDIR=$ZDOTDIR
1128 ZDOTDIR=$FARADAYZDOTDIR /bin/zsh
12 #source ~/.faraday/zsh/.zshrc
29
30 #source ~/.faraday/zsh/.zshrc
6565 FARADAY_QTRCBAK = os.path.expanduser(CONST_FARADAY_QTRC_BACKUP)
6666 CONST_VERSION_FILE = os.path.join(FARADAY_BASE,"VERSION")
6767
68 REQUESTS_CA_BUNDLE_VAR = "REQUESTS_CA_BUNDLE"
69 FARADAY_DEFAULT_PORT_XMLRPC = 9876
70 FARADAY_DEFAULT_PORT_REST = 9977
71 FARADAY_DEFAULT_HOST = "localhost"
72
73
6874 def getParserArgs():
6975 """Parser setup for faraday launcher arguments.
7076
8086
8187 parser_connection.add_argument('-n', '--hostname', action="store",
8288 dest="host",
83 default="localhost",
84 help="The hostname where api XMLRPCServer will listen. \
89 default=None,
90 help="The hostname where both server APIs will listen (XMLRPC and RESTful). \
8591 Default = localhost")
8692
87 parser_connection.add_argument('-p', '--port', action="store", dest="port",
88 default=9876, type=int,
89 help="Sets the port where api XMLRPCServer will listen. Default = 9876")
93 parser_connection.add_argument('-px', '--port-xmlrpc', action="store", dest="port_xmlrpc", default=None, type=int,
94 help="Sets the port where the api XMLRPCServer will listen. Default = 9876")
95 parser_connection.add_argument('-pr', '--port-rest', action="store", dest="port_rest",
96 default=None, type=int,
97 help="Sets the port where the api RESTful server will listen. Default = 9977")
9098
9199 parser.add_argument('-d', '--debug', action="store_true", dest="debug",
92100 default=False,
150158 #args = parser.parse_args(['@parser_args.cfg'])
151159 return parser.parse_args()
152160
161
153162 def query_user_bool(question, default=True):
154163 """Returns a boolean based on user input.
155164
185194 if choice in valid_no_ans:
186195 return False
187196
188 sys.stdout.write("Please respond with 'yes' or 'no' "\
189 "(or 'y' or 'n').\n")
197 sys.stdout.write("Please respond with 'yes' or 'no' "
198 "(or 'y' or 'n').\n")
190199
191200
192201 def checkDependencies():
215224 try:
216225 __import__(module[0])
217226 except ImportError:
218 if query_user_bool("Missing module %s." \
227 if query_user_bool("Missing module %s."
219228 " Do you wish to install it?" % module[0]):
220229 pip.main(['install', "%s==%s" %
221230 (module[0], module[1]), '--user'])
236245
237246 """
238247
239 logger.warning("[!] Faraday will be started with a profiler attached." \
240 "Performance may be affected.")
248 logger.warning("[!] Faraday will be started with a profiler attached."
249 "Performance may be affected.")
241250
242251 start = profile(app,
243 filename=output,
244 entries=depth)
252 filename=output,
253 entries=depth)
245254 return start
255
246256
247257 def setConf():
248258 """User configuration management and instantiation.
256266
257267 CONF = getInstanceConfiguration()
258268 CONF.setDebugStatus(args.debug)
259 if args.host != 'localhost':
260 CONF.setApiConInfoHost(args.host)
261 if args.port != 9876:
262 CONF.setApiConInfoPort(args.port)
269
270 host = CONF.getApiConInfoHost() if str(CONF.getApiConInfoHost()) != "None" else FARADAY_DEFAULT_HOST
271 port_xmlrpc = CONF.getApiConInfoPort() if str(CONF.getApiConInfoPort()) != "None" else FARADAY_DEFAULT_PORT_XMLRPC
272 port_rest = CONF.getApiRestfulConInfoPort() if str(CONF.getApiRestfulConInfoPort()) != "None" else FARADAY_DEFAULT_PORT_REST
273
274 host = args.host if args.host else host
275 port_xmlrpc = args.port_xmlrpc if args.port_xmlrpc else port_xmlrpc
276 port_rest = args.port_rest if args.port_rest else port_rest
277
278 logger.info("XMLRPC API Server listening on %s:%s" % (host, port_xmlrpc))
279 logger.info("RESTful API Server listening on %s:%s" % (host, port_rest))
280
281 CONF.setApiConInfoHost(host)
282 CONF.setApiConInfoPort(port_xmlrpc)
283 CONF.setApiRestfulConInfoPort(port_rest)
284
263285 CONF.setAuth(args.disable_login)
264286
265287
288310
289311 if args.profile:
290312 logger.info("Starting main application with profiler.")
291 start = startProfiler(
292 main_app.start,
293 args.profile_output,
294 args.profile_depth)
313 start = startProfiler(main_app.start,
314 args.profile_output,
315 args.profile_depth)
295316 else:
296317 logger.info("Starting main application.")
297318 start = main_app.start
306327 """Make sure you got couchdb up and running.\nIf couchdb is up, point your browser to: \n[%s]""" % url)
307328 else:
308329 print(Fore.WHITE + Style.BRIGHT + \
309 """Please config Couchdb for fancy HTML5 Dashboard""")
330 """Please config Couchdb for fancy HTML5 Dashboard (https://github.com/infobyte/faraday/wiki/Couchdb)""")
310331
311332 print(Fore.RESET + Back.RESET + Style.RESET_ALL)
312333
313334 exit_status = start()
314335
315336 return exit_status
337
316338
317339 def setupPlugins(dev_mode=False):
318340 """Checks and handles Faraday's plugin status.
339361 logger.info("No plugins folder detected. Creating new one.")
340362
341363 shutil.copytree(FARADAY_PLUGINS_BASEPATH, FARADAY_PLUGINS_PATH)
364
342365
343366 def setupQtrc():
344367 """Cheks and handles QT configuration file.
356379 except:
357380 pass
358381
382
359383 def setupZSH():
360384 """Cheks and handles Faraday's integration with ZSH.
361385
378402 shutil.copy(FARADAY_BASE_ZSH, FARADAY_USER_ZSH_PATH)
379403 shutil.copy(FARADAY_BASE_ZSH_PLUGIN, FARADAY_USER_ZSH_PATH)
380404
405
381406 def setupXMLConfig():
382407 """Checks user configuration file status.
383408
389414 shutil.copy(FARADAY_BASE_CONFIG_XML, FARADAY_USER_CONFIG_XML)
390415 else:
391416 logger.info("Using custom user configuration.")
417
392418
393419 def setupLibs():
394420 """Checks ELF libraries status."
423449
424450 subprocess.call(['ln', '-s', helpers, FARADAY_BASE_LIB_HELPERS])
425451
452
426453 def setupImages():
427454 """ Copy png icons
428455 """
430457 shutil.rmtree(FARADAY_USER_IMAGES)
431458 shutil.copytree(FARADAY_BASE_IMAGES, FARADAY_USER_IMAGES)
432459
460
433461 def checkConfiguration():
434462 """Checks if the environment is ready to run Faraday.
435463
441469 logger.info("Checking configuration.")
442470 logger.info("Setting up plugins.")
443471 setupPlugins(args.dev_mode)
444 logger.info("Setting up folders.")
445 setupFolders(CONST_FARADAY_FOLDER_LIST)
446472 logger.info("Setting up Qt configuration.")
447473 setupQtrc()
448474 logger.info("Setting up ZSH integration.")
454480 logger.info("Setting up icons for QT interface.")
455481 setupImages()
456482
483
457484 def setupFolders(folderlist):
458485 """Checks if a list of folders exists and creates them otherwise.
459486
463490 fp_folder = os.path.join(FARADAY_USER_HOME, folder)
464491 checkFolder(fp_folder)
465492
493
466494 def checkFolder(folder):
467495 """Checks whether a folder exists and creates it if it doesn't.
468496
469497 """
470498
471499 if not os.path.isdir(folder):
472 logger.info("Creating %s" % folder)
473 os.mkdir(folder)
500 if logger:
501 logger.info("Creating %s" % folder)
502 os.makedirs(folder)
503
504
474505
475506 def printBanner():
476507 """Prints Faraday's ascii banner.
491522 print(Back.RESET + " Where pwnage goes multiplayer")
492523 print(Fore.RESET + Back.RESET + Style.RESET_ALL)
493524 logger.info("Starting Faraday IDE.")
525
494526
495527 def update():
496528 """Updates Faraday IDE.
563595 except Exception as e:
564596 getLogger("launcher").error("It seems that something's wrong with your version\nPlease contact customer support")
565597 exit(-1)
566
598
567599
568600 def init():
569601 """Initializes what is needed before starting.
574606
575607 global args
576608 global logger
609 logger = None
577610
578611 args = getParserArgs()
612 setupFolders(CONST_FARADAY_FOLDER_LIST)
613 setUpLogger(args.debug)
579614 logger = getLogger("launcher")
615
580616
581617 def main():
582618 """Main.
590626 printBanner()
591627 logger.info("Dependencies met.")
592628 if args.cert_path:
593 os.environ['REQUESTS_CA_BUNDLE'] = args.cert_path
629 os.environ[REQUESTS_CA_BUNDLE_VAR] = args.cert_path
594630 checkConfiguration()
595631 setConf()
596632 checkCouchUrl()
597633 checkVersion()
598 setUpLogger()
599634 update()
600635 checkUpdates()
601636 startFaraday()
1717 from gui.gui_app import FaradayUi
1818 from gui.qt3.mainwindow import MainWindow
1919 from gui.qt3.customevents import QtCustomEvent
20 from gui.qt3.logconsole import GUIHandler
2021 from shell.controller.env import ShellEnvironment
2122
2223 import model.guiapi
2324 import model.api
2425 import model.log
26 from utils.logs import addHandler
2527
2628 from config.configuration import getInstanceConfiguration
2729 CONF = getInstanceConfiguration()
4951 notifier.widget = self._main_window
5052 model.guiapi.notification_center.registerWidget(self._main_window)
5153
54 self.loghandler = GUIHandler()
55 addHandler(self.loghandler)
56
5257 self._splash_screen = qt.QSplashScreen(
5358 qt.QPixmap(os.path.join(CONF.getImagePath(), "splash2.png")),
5459 qt.Qt.WStyle_StaysOnTop)
6671 model.api.log("Make sure you have couchdb up and running if you want visualizations.")
6772 model.api.log("If couchdb is up, point your browser to: [%s]" % url)
6873 else:
69 model.api.log("Please configure Couchdb for fancy HTML5 Dashboard")
74 model.api.log("Please configure Couchdb for fancy HTML5 Dashboard (https://github.com/infobyte/faraday/wiki/Couchdb)")
7075 exit_code = self.exec_loop()
7176 return exit_code
7277
7378 def createLoggerWidget(self):
74 if not model.log.getLogger().isGUIOutputRegistered():
75 model.log.getLogger().registerGUIOutput(self._main_window.getLogConsole())
79 self.loghandler.registerGUIOutput(self._main_window.getLogConsole())
7680
7781 def loadWorkspaces(self):
7882 self.getMainWindow().getWorkspaceTreeView().loadAllWorkspaces()
9498 self._splash_screen.finish(self._main_window)
9599
96100 def quit(self):
97 model.log.getLogger().clearWidgets()
101 self.loghandler.clearWidgets()
98102 self.getMainWindow().hide()
99103 envs = [env for env in self._shell_envs.itervalues()]
100104 for env in envs:
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 import qt
7 import os
8 from gui.qt3.dialogs import BaseDialog
9
10 from config.configuration import getInstanceConfiguration
11 CONF = getInstanceConfiguration()
12
13
14 class ConfigurationPage(qt.QVBox):
15 def __init__(self, parent=None):
16 super(ConfigurationPage, self).__init__(parent)
17 configGroup = qt.QHGroupBox("Server configuration", self)
18 serverLabel = qt.QLabel("Server:", configGroup)
19 serverCombo = qt.QComboBox(configGroup)
20 serverCombo.insertItem("Trolltech (Australia)")
21 serverCombo.insertItem("Trolltech (Germany)")
22 serverCombo.insertItem("Trolltech (Norway)")
23 serverCombo.insertItem("Trolltech (People's Republic of China)")
24 serverCombo.insertItem("Trolltech (USA)")
25
26
27 class UpdatePage(qt.QVBox):
28 def __init__(self, parent=None):
29 super(UpdatePage, self).__init__(parent)
30
31 updateGroup = qt.QVGroupBox("Package selection", self)
32 systemCheckBox = qt.QCheckBox("Update system", updateGroup)
33 appsCheckBox = qt.QCheckBox("Update applications", updateGroup)
34 docsCheckBox = qt.QCheckBox("Update documentation", updateGroup)
35
36 packageGroup = qt.QHGroupBox("Existing packages", self)
37 packageList = qt.QListView(packageGroup)
38 packageList.addColumn("")
39 packageList.setColumnWidthMode(0, qt.QListView.Maximum)
40 packageList.setColumnWidth(0, packageList.width())
41
42 qtItem = qt.QListViewItem(packageList)
43 qtItem.setText(0, "Qt")
44 qsaItem = qt.QListViewItem(packageList)
45 qsaItem.setText(0, "QSA")
46 teamBuilderItem = qt.QListViewItem(packageList)
47 teamBuilderItem.setText(0, "Teambuilder")
48 self.setSpacing(12)
49 startUpdateButton = qt.QPushButton("Start update", self)
50
51
52
53 class ConfigDialog(BaseDialog):
54 def __init__(self, parent=None, configuration=None):
55 BaseDialog.__init__(self, parent, "Config Dialog",
56 layout_margin=10, layout_spacing=15, modal=True)
57
58 self.configuration = configuration
59
60 hbox = qt.QHBox(self)
61
62 self.contentsWidget = qt.QIconView(hbox)
63
64
65
66
67 self.contentsWidget.setMaximumWidth(128)
68
69
70 self.pagesWidget = qt.QWidgetStack(hbox)
71 self.pagesWidget.addWidget(ConfigurationPage(), 0)
72 self.pagesWidget.addWidget(UpdatePage(), 1)
73 self.pagesWidget.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding))
74
75
76 self.createIcons()
77 hbox.setStretchFactor(self.contentsWidget, 0)
78 hbox.setStretchFactor(self.pagesWidget, 10)
79 self.layout.addWidget(hbox)
80
81
82 self.setupButtons({
83 "ok": None,
84 "cancel" : self.close,
85 })
86
87
88 def changePage(self, item):
89 if item:
90 current = self.contentsWidget.currentItem()
91 self.pagesWidget.raiseWidget(self.contentsWidget.index(current))
92
93 def createIcons(self):
94 configButton = qt.QIconViewItem(self.contentsWidget)
95 configButton.setPixmap(qt.QPixmap(os.path.join(CONF.getIconsPath(), 'config.png')))
96 configButton.setText("Configuration")
97
98
99
100
101 updateButton = qt.QIconViewItem(self.contentsWidget)
102 updateButton.setPixmap(qt.QPixmap(os.path.join(CONF.getIconsPath(), 'update.png')))
103 updateButton.setText("Update")
104
105
106
107 self.connect(self.contentsWidget, qt.SIGNAL('clicked(QIconViewItem*)'), self.changePage)
108
109 def sizeHint(self):
110 return qt.QSize(600, 350)
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 import qt
7 import os
8 from gui.qt3.dialogs import BaseDialog
9
10 from config.configuration import getInstanceConfiguration
11 CONF = getInstanceConfiguration()
12
13
14 class ConfigurationPage(qt.QVBox):
15 def __init__(self, parent=None):
16 super(ConfigurationPage, self).__init__(parent)
17 configGroup = qt.QHGroupBox("Server configuration", self)
18 serverLabel = qt.QLabel("Server:", configGroup)
19 serverCombo = qt.QComboBox(configGroup)
20 serverCombo.insertItem("Trolltech (Australia)")
21 serverCombo.insertItem("Trolltech (Germany)")
22 serverCombo.insertItem("Trolltech (Norway)")
23 serverCombo.insertItem("Trolltech (People's Republic of China)")
24 serverCombo.insertItem("Trolltech (USA)")
25
26
27 class UpdatePage(qt.QVBox):
28 def __init__(self, parent=None):
29 super(UpdatePage, self).__init__(parent)
30
31 updateGroup = qt.QVGroupBox("Package selection", self)
32 systemCheckBox = qt.QCheckBox("Update system", updateGroup)
33 appsCheckBox = qt.QCheckBox("Update applications", updateGroup)
34 docsCheckBox = qt.QCheckBox("Update documentation", updateGroup)
35
36 packageGroup = qt.QHGroupBox("Existing packages", self)
37 packageList = qt.QListView(packageGroup)
38 packageList.addColumn("")
39 packageList.setColumnWidthMode(0, qt.QListView.Maximum)
40 packageList.setColumnWidth(0, packageList.width())
41
42 qtItem = qt.QListViewItem(packageList)
43 qtItem.setText(0, "Qt")
44 qsaItem = qt.QListViewItem(packageList)
45 qsaItem.setText(0, "QSA")
46 teamBuilderItem = qt.QListViewItem(packageList)
47 teamBuilderItem.setText(0, "Teambuilder")
48 self.setSpacing(12)
49 startUpdateButton = qt.QPushButton("Start update", self)
50
51
52
53 class ConfigDialog(BaseDialog):
54 def __init__(self, parent=None, configuration=None):
55 BaseDialog.__init__(self, parent, "Config Dialog",
56 layout_margin=10, layout_spacing=15, modal=True)
57
58 self.configuration = configuration
59
60 hbox = qt.QHBox(self)
61
62 self.contentsWidget = qt.QIconView(hbox)
63
64
65
66
67 self.contentsWidget.setMaximumWidth(128)
68
69
70 self.pagesWidget = qt.QWidgetStack(hbox)
71 self.pagesWidget.addWidget(ConfigurationPage(), 0)
72 self.pagesWidget.addWidget(UpdatePage(), 1)
73 self.pagesWidget.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding))
74
75
76 self.createIcons()
77 hbox.setStretchFactor(self.contentsWidget, 0)
78 hbox.setStretchFactor(self.pagesWidget, 10)
79 self.layout.addWidget(hbox)
80
81
82 self.setupButtons({
83 "ok": None,
84 "cancel" : self.close,
85 })
86
87
88 def changePage(self, item):
89 if item:
90 current = self.contentsWidget.currentItem()
91 self.pagesWidget.raiseWidget(self.contentsWidget.index(current))
92
93 def createIcons(self):
94 configButton = qt.QIconViewItem(self.contentsWidget)
95 configButton.setPixmap(qt.QPixmap(os.path.join(CONF.getIconsPath(), 'config.png')))
96 configButton.setText("Configuration")
97
98
99
100
101 updateButton = qt.QIconViewItem(self.contentsWidget)
102 updateButton.setPixmap(qt.QPixmap(os.path.join(CONF.getIconsPath(), 'update.png')))
103 updateButton.setText("Update")
104
105
106
107 self.connect(self.contentsWidget, qt.SIGNAL('clicked(QIconViewItem*)'), self.changePage)
108
109 def sizeHint(self):
110 return qt.QSize(600, 350)
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import os
9 import qt
10 import model.api as api
11 import model.guiapi as guiapi
12 import re
13 import model.hosts as hosts
14 from managers.model_managers import WorkspaceManager
15 from ui.plugin_settings import *
16 from ui.vulnerabilities import *
17 from ui.preferences import *
18 from ui.noteslist import NotesListUI
19 from ui.evidenceslist import *
20 from edition import EditionTable, HostEditor, ServiceEditor, InterfaceEditor, NoteEditor, NewNoteDialog, VulnEditor, NewVulnDialog, VulnWebEditor, NewCredDialog, CredEditor
21 from modelobjectitems import NoteRootItem, VulnRootItem, CredRootItem
22
23 from config.configuration import getInstanceConfiguration
24 CONF = getInstanceConfiguration()
25
26
27
28 class LogConsole(qt.QVBox):
29 """
30 widget component used to display a log or any other content in
31 a small console window
32 """
33 tag_regex = re.compile(r"^(\[(.*)\]\s+-).*$", re.DOTALL)
34 tag_replace_regex = re.compile(r"^(\[(.*)\]\s+-)")
35 tag_colors = {
36 "NOTIFICATION" : "#1400F2",
37 "INFO" : "#000000",
38 "WARNING" : "#F5760F",
39 "ERROR" : "#FC0000",
40 "CRITICAL": "#FC0000",
41 "DEBUG" : "#0AC400",
42 }
43
44 def __init__(self, parent, caption=""):
45 qt.QVBox.__init__(self, parent)
46 self.setName(caption)
47 self._text_edit = qt.QTextEdit(self, caption)
48
49 self._text_edit.setTextFormat(qt.Qt.LogText)
50
51 def customEvent(self, event):
52 self.update(event)
53
54 def update(self, event):
55 if event.type() == 3131:
56 self.appendText(event.text)
57
58 def appendText(self, text):
59 """
60 appends new text to the console
61 """
62 m = self.tag_regex.match(text)
63 if m is not None:
64 tag = m.group(2).upper()
65 colored_tag = "<font color=\"%s\"><b>[%s]</b></font> -" % (self.tag_colors.get(tag, "#000000"), tag)
66 text = self.tag_replace_regex.sub(colored_tag, text)
67 else:
68 text = "<font color=\"#000000\"><b>[INFO]</b></font> - %s" % text
69
70 self._text_edit.append(text)
71
72 def clear(self):
73 """
74 Clear the console
75 """
76 self._text_edit.clear()
77
78 def sizeHint(self):
79 """Returns recommended size of dialog."""
80 return qt.QSize(90, 30)
81
82
83
84 class BaseDialog(qt.QDialog):
85
86 def __init__(self, parent, name, layout_margin=0, layout_spacing=-1, modal=True):
87 qt.QDialog.__init__(self, parent, name, modal)
88 if layout_spacing == -1:
89 layout_spacing = self.fontMetrics().height()
90
91 self.layout = qt.QVBoxLayout(self, layout_margin, layout_spacing, "layout")
92 self.button_box = None
93 self.ok_button = None
94 self.cancel_button = None
95 self.quit_button = None
96
97 def setupButtons(self, buttons=None):
98 """
99 Creates and setup buttons clicked singal connection using callbacks provided
100 The buttons parameter must be a dict with keys (in lowercase)
101 "ok", "cancel" or "quit" and a callback reference as value.
102 If None is provided as callback value then default behaviour will
103 be applied to the button (accept for Ok, reject for cancel and quit)
104 Button order will be always Ok, Cancel and Quit
105 This will add only the buttons provided in the buttons parameter, so not all
106 keys must be used. You can add only the ones needed.
107 If no parameter is provided, OK and Cancel buttons will be added with
108 their default behaviour.
109 IMPORTANT: if callbacks do not call accept or reject methods, then
110 the dialog won't end and will be visible. Remember to call accept &
111 reject internally on your provided callbacks
112 """
113 self.button_box = qt.QHBoxLayout(self.layout)
114 spacer = qt.QSpacerItem(0,0,qt.QSizePolicy.Expanding,qt.QSizePolicy.Minimum)
115 self.button_box.addItem(spacer)
116
117 if buttons is None:
118 self._addOkButton()
119 self._addCancelButton()
120 else:
121 if "ok" in buttons:
122 self._addOkButton(buttons["ok"])
123 if "cancel" in buttons:
124 self._addCancelButton(buttons["cancel"])
125 if "quit" in buttons:
126 self._addQuitButton(buttons["quit"])
127
128 def _addOkButton(self, callback=None):
129 self.ok_button = qt.QPushButton( "OK", self )
130 self.button_box.addWidget( self.ok_button )
131 if callback is None:
132 callback = self.accept
133 self.connect( self.ok_button, qt.SIGNAL('clicked()'), callback )
134
135 def _addCancelButton(self, callback=None):
136 self.cancel_button = qt.QPushButton("Cancel", self)
137 self.button_box.addWidget( self.cancel_button )
138 if callback is None:
139 callback = self.reject
140 self.connect( self.cancel_button, qt.SIGNAL('clicked()'), callback)
141
142 def _addQuitButton(self, callback=None):
143 self.quit_button = qt.QPushButton("Quit", self)
144 self.button_box.addWidget( self.quit_button )
145 if callback is None:
146 callback = self.reject
147 self.connect( self.quit_button, qt.SIGNAL('clicked()'), callback)
148
149 def sizeHint(self):
150
151 return qt.QSize(400, 150)
152
153
154
155 class LoginDialog(BaseDialog):
156 def __init__(self, parent, callback):
157 BaseDialog.__init__(self, parent, "Login",
158 layout_margin=10, layout_spacing=15, modal=True)
159
160 self._auth_callback = callback
161 self.setCaption("Login")
162
163 hbox1 = qt.QHBox(self)
164 hbox1.setSpacing(5)
165 self._username_label = qt.QLabel("Username", hbox1)
166 self._username_edit = qt.QLineEdit(hbox1)
167 self.layout.addWidget(hbox1)
168
169 hbox2 = qt.QHBox(self)
170 hbox2.setSpacing(10)
171 self._password_label = qt.QLabel("Password", hbox2)
172 self.__password_edit = qt.QLineEdit(hbox2)
173 self.__password_edit.setEchoMode(qt.QLineEdit.Password)
174 self.layout.addWidget(hbox2)
175
176 self.__username_txt = self._username_edit.text()
177 self.__passwd_txt = self.__password_edit.text()
178
179 self.setupButtons({
180 "ok" : self._login,
181 "cancel" : self._clear,
182 "quit" : None,
183 })
184
185 def getData(self):
186 self.__username_txt = self._username_edit.text()
187 self.__passwd_txt = self.__password_edit.text()
188 return self.__username_txt.latin1(), self.__passwd_txt.latin1()
189
190 def _login(self):
191
192
193 self.__username_txt = self._username_edit.text()
194 self.__passwd_txt = self.__password_edit.text()
195 api.devlog("Username: %s\nPassword: %s" %(self.__username_txt, self.__passwd_txt))
196 self.accept()
197
198 def _clear(self):
199
200 self._username_edit.clear()
201 self.__password_edit.clear()
202
203 def sizeHint(self):
204 return qt.QSize(250, 100)
205
206
207
208 class DebugPersistenceDialog(BaseDialog):
209
210 def __init__(self, parent):
211 BaseDialog.__init__(self, parent, "PersistenceDebugDialog",
212 layout_margin=15, layout_spacing=10, modal=True)
213 self.setCaption( 'Persistence Debug Dialog' )
214
215 self.layout.addWidget( self.logolabel )
216 self.layout.addWidget( self.text )
217 self.setupButtons({"ok" : None})
218
219
220
221 class ConflictResolutionDialog(BaseDialog):
222 def __init__(self, conflicts, parent=None, name=None):
223
224 BaseDialog.__init__(self, parent, "Conflicts",
225 layout_margin=10, layout_spacing=15, modal=True)
226
227 self.conflict = None
228 self.conflict_iterator = iter(conflicts)
229 self.first_object = None
230 self.second_object = None
231
232 hbox = qt.QHBoxLayout()
233
234 vbox = qt.QVBoxLayout()
235 self.label_first_object = qt.QLabel("", self)
236 vbox.addWidget(self.label_first_object)
237 self.detailtable_first_object = EditionTable(self)
238 self.editor_first_object = None
239 vbox.addWidget(self.detailtable_first_object)
240 self.choice_button_first_object = qt.QRadioButton(self, "")
241 vbox.addWidget(self.choice_button_first_object)
242
243 hbox.addLayout(vbox)
244
245 vbox = qt.QVBoxLayout()
246 self.label_second_object = qt.QLabel("", self)
247 vbox.addWidget(self.label_second_object)
248 self.detailtable_second_object = EditionTable(self)
249 self.editor_second_object = None
250 vbox.addWidget(self.detailtable_second_object)
251 self.choice_button_second_object = qt.QRadioButton(self, "")
252 vbox.addWidget(self.choice_button_second_object)
253
254 self.object_group_button = qt.QButtonGroup()
255 self.object_group_button.insert(self.choice_button_first_object)
256 self.object_group_button.insert(self.choice_button_second_object)
257
258 hbox.addLayout(vbox)
259
260 self.layout.addLayout(hbox)
261
262 self.setupButtons({"ok": self.resolve, "cancel": self.quit})
263
264 self.del_callback = None
265 self.add_callback = None
266
267 self.setup()
268
269 def setup(self):
270 self.getNextConflict()
271 if self.conflict:
272 self.setupConflict()
273 else:
274 self.accept()
275
276 def getNextConflict(self):
277 try:
278 self.conflict = self.conflict_iterator.next()
279 except StopIteration:
280 self.conflict = None
281
282 def setupConflict(self):
283 if not self.conflict:
284 return
285
286 self.first_object = self.conflict.getFirstObject()
287 self.second_object = self.conflict.getSecondObject()
288 type = self.conflict.getModelObjectType()
289
290 self.setCaption(type)
291 name_first_object = self.first_object.getName()
292 name_second_object = self.second_object.getName()
293 if self.first_object.getParent() is not None:
294 name_first_object += " (Host: %s)" % self.first_object.getHost().getName()
295 name_second_object += " (Host: %s)" % self.first_object.getHost().getName()
296 self.label_first_object.setText(name_first_object)
297 self.label_second_object.setText(name_second_object)
298
299 if type == "Host":
300 self.editor_first_object = HostEditor(self.first_object)
301 self.editor_second_object = HostEditor(self.second_object)
302 elif type == "Interface":
303 self.editor_first_object = InterfaceEditor(self.first_object)
304 self.editor_second_object = InterfaceEditor(self.second_object)
305 elif type == "Service":
306 self.editor_first_object = ServiceEditor(self.first_object)
307 self.editor_second_object = ServiceEditor(self.second_object)
308
309 self.editor_first_object.fillEditionTable(self.detailtable_first_object)
310
311 self.editor_second_object.fillEditionTable(self.detailtable_second_object)
312
313
314 def getSelectedEditor(self):
315 if self.choice_button_first_object.isChecked():
316 editor = self.editor_first_object
317 elif self.choice_button_second_object.isChecked():
318 editor = self.editor_second_object
319 else:
320 editor = None
321 return editor
322
323 def resolve(self):
324 editor_selected = self.getSelectedEditor()
325 if editor_selected:
326 guiapi.resolveConflict(self.conflict, editor_selected.getArgs())
327 self.setup()
328
329 def quit(self):
330 self.reject()
331
332 def sizeHint(self):
333 return qt.QSize(750, 500)
334
335
336
337 class ModelObjectListViewItem(qt.QListViewItem):
338 def __init__(self, qtparent, model_object=None):
339 qt.QListViewItem.__init__(self, qtparent)
340 self.model_object = model_object
341 if self.model_object:
342 self.setText(0, model_object.name)
343 else:
344 self.setText(0, "")
345
346 def getModelObject(self):
347 return self.model_object
348
349 class ListableObjecttDialog(BaseDialog):
350 def __init__(self, parent=None, title=None, model_object=None, objects_list = [], layout_margin=10, layout_spacing=15, modal=True):
351 BaseDialog.__init__(self, parent, title,
352 layout_margin=10, layout_spacing=15, modal=True)
353
354 hbox = qt.QHBoxLayout()
355 vbox1 = qt.QVBoxLayout()
356 vbox1.setMargin(5)
357 vbox2 = qt.QVBoxLayout()
358 vbox2.setMargin(5)
359 self.model_object = model_object
360 self.objects_list = objects_list
361 self._selected_object = None
362 self._current_item = None
363 self._selected_items = []
364 self.edition_layout = None
365 self.title = title
366
367 self.listview = qt.QListView(self)
368 self.listview.setSorting(-1)
369 self.listview.setSelectionMode(qt.QListView.Extended)
370 self.connect(self.listview, qt.SIGNAL("selectionChanged()"), self._itemSelected)
371 self.listview.addColumn(title, self.listview.size().width())
372 self.listview.setColumnWidthMode(0, qt.QListView.Maximum)
373 self.setListItems()
374
375 vbox1.addWidget(self.listview)
376
377 self.button_box1 = qt.QHBoxLayout()
378 self.add_button = qt.QPushButton("Add", self)
379 self.add_button.setMaximumSize(qt.QSize(100, 25))
380 self.connect(self.add_button, qt.SIGNAL('clicked()'), self.addValue)
381 self.remove_button = qt.QPushButton("Remove", self)
382 self.remove_button.setMaximumSize(qt.QSize(100, 25))
383 self.connect(self.remove_button, qt.SIGNAL('clicked()'), self.removeValue)
384 self.button_box1.addWidget(self.add_button)
385 self.button_box1.addWidget(self.remove_button)
386 self.button_box1.addStretch(1)
387
388 vbox1.addLayout(self.button_box1)
389
390 self.setupEditor(vbox2)
391
392 self.button_box2 = qt.QHBoxLayout()
393 self.save_button = qt.QPushButton("Save", self)
394 self.save_button.setMaximumSize(qt.QSize(100, 25))
395 self.connect(self.save_button, qt.SIGNAL('clicked()'), self.saveValue)
396 self.button_box2.addWidget(self.save_button)
397 self.button_box2.addStretch(1)
398
399 vbox2.addLayout(self.button_box2)
400
401 hbox.setSpacing(6)
402 hbox.addLayout(vbox1)
403 hbox.addLayout(vbox2)
404 self.layout.addLayout(hbox)
405
406 self.setupButtons({"quit": None})
407
408 def setupEditor(self, parent_layout):
409 pass
410
411 def saveValue(self):
412 pass
413
414 def addValue(self):
415 pass
416
417 def removeValue(self):
418 pass
419
420 def _itemSelected(self):
421 self.edition_layout.clear()
422 self._current_item = self.listview.currentItem()
423 i = self.listview.firstChild()
424 self._selected_items=[]
425 while i is not None:
426 if i.isSelected():
427 self._selected_items.append(i)
428 i = i.itemBelow()
429 self.setEdition()
430
431 def sizeHint(self):
432 return qt.QSize(750, 500)
433
434 class NotesDialog(ListableObjecttDialog):
435 def __init__(self, parent=None, model_object=None):
436 ListableObjecttDialog.__init__(self, parent, "Notes", model_object, model_object.getNotes(),
437 layout_margin=10, layout_spacing=15, modal=True)
438
439 def setupEditor(self, parent_layout):
440 self.edition_layout = NoteEditor(self)
441 parent_layout.addLayout(self.edition_layout)
442
443 def setListItems(self):
444 self.listview.clear()
445 self.rootitem = NoteRootItem(self.listview, self.title, self.model_object)
446 for obj in self.model_object.getNotes():
447 self.rootitem.addNote(obj)
448
449 def setEdition(self):
450 if self._current_item is not None:
451 if self._current_item.type == "Note":
452 self.edition_layout.setNote(self._current_item.getModelObject())
453
454 def saveValue(self):
455 if self._current_item is not None:
456 if self._current_item.type == "Note":
457 note = self._current_item.getModelObject()
458 kwargs = self.edition_layout.getArgs()
459 if kwargs["name"] and kwargs["text"]:
460 guiapi.editNote(note, **kwargs)
461 self.setListItems()
462
463 def addValue(self):
464 dialog = NewNoteDialog(self, callback=self.__addValue)
465 dialog.exec_loop()
466
467 def __addValue(self, *args):
468 obj = self.rootitem.getModelObject()
469 if self._current_item:
470 obj = self._current_item.getModelObject()
471 guiapi.createAndAddNote(obj, *args)
472 self.setListItems()
473
474 def removeValue(self):
475 for item in self._selected_items:
476 if item.type == "Note":
477 note = item.getModelObject()
478 guiapi.delNote(note.getParent().getID(), note.getID())
479 self.setListItems()
480 self.edition_layout.clear()
481
482
483 def sizeHint(self):
484 return qt.QSize(750, 500)
485
486
487 class VulnsDialog(ListableObjecttDialog):
488 def __init__(self, parent=None, model_object=None):
489 ListableObjecttDialog.__init__(self, parent, "Vulns", model_object, model_object.getVulns(),
490 layout_margin=10, layout_spacing=15, modal=True)
491
492 def setupEditor(self, parent_layout):
493 self._widget_stack = qt.QWidgetStack(self)
494
495 self._vuln_edition_widget = qt.QFrame()
496 self._vuln_edition_layout = VulnEditor(self._vuln_edition_widget)
497
498 self._vuln_web_scrollbar_view = qt.QScrollView()
499 self._vuln_web_scrollbar_view.setResizePolicy(qt.QScrollView.AutoOneFit)
500 self._vuln_web_edition_widget = qt.QFrame(self._vuln_web_scrollbar_view.viewport())
501 self._vuln_web_edition_layout = VulnWebEditor(self._vuln_web_edition_widget)
502 self._vuln_web_edition_layout.setMargin(5)
503 self._vuln_web_scrollbar_view.addChild(self._vuln_web_edition_widget)
504
505 self._widget_stack.addWidget(self._vuln_edition_widget, 0)
506 self._widget_stack.addWidget(self._vuln_web_scrollbar_view, 1)
507 self._widget_stack.raiseWidget(self._vuln_edition_widget)
508
509 self._vuln_edition_widget.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Ignored))
510 self._vuln_web_edition_widget.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Ignored))
511
512 self.edition_layout = self._vuln_edition_widget.layout()
513
514 parent_layout.addWidget(self._widget_stack)
515
516 def setListItems(self):
517 self.listview.clear()
518 self.rootitem = VulnRootItem(self.listview, self.title, self.model_object)
519 for obj in self.model_object.getVulns():
520 self.rootitem.addVuln(obj)
521
522 def setEdition(self):
523 if self._current_item is not None:
524 if self._current_item.type == "Vuln" or self._current_item.type == "VulnWeb":
525 widget = self._vuln_edition_widget
526 self._widget_stack.raiseWidget(widget)
527 if self._current_item.type == "VulnWeb":
528 widget = self._vuln_web_edition_widget
529 self._widget_stack.raiseWidget(self._vuln_web_scrollbar_view)
530 self.edition_layout = widget.layout()
531 self.edition_layout.setVuln(self._current_item.getModelObject())
532
533 def saveValue(self):
534 if self._current_item is not None:
535 if self._current_item.type == "Vuln" or self._current_item.type == "VulnWeb":
536 vuln = self._current_item.getModelObject()
537 kwargs = self.edition_layout.getArgs()
538 if kwargs["name"] and kwargs["desc"]:
539 if self._current_item.type == "Vuln":
540 guiapi.editVuln(vuln, **kwargs)
541 else:
542 guiapi.editVulnWeb(vuln, **kwargs)
543 self.setListItems()
544
545 def addValue(self):
546 vuln_web_enabled = False
547 if self.model_object.class_signature == "Service":
548 vuln_web_enabled = True
549 dialog = NewVulnDialog(self, callback=self.__addValue, vuln_web_enabled=vuln_web_enabled)
550 dialog.exec_loop()
551
552 def __addValue(self, *args):
553 obj = self.model_object
554 if args[0]:
555
556 guiapi.createAndAddVulnWeb(obj, *args[1:])
557 else:
558 guiapi.createAndAddVuln(obj, *args[1:])
559 self.setListItems()
560
561 def removeValue(self):
562 for item in self._selected_items:
563 if item.type == "Vuln" or item.type == "VulnWeb":
564 vuln = item.getModelObject()
565 guiapi.delVuln(vuln.getParent().getID(), vuln.getID())
566 self.setListItems()
567 self.edition_layout.clear()
568
569 def sizeHint(self):
570 return qt.QSize(850, 500)
571
572
573 class CredsDialog(ListableObjecttDialog):
574 def __init__(self, parent=None, model_object=None):
575 ListableObjecttDialog.__init__(self, parent, "Credentials", model_object, model_object.getCreds(),
576 layout_margin=10, layout_spacing=15, modal=True)
577
578 def setupEditor(self, parent_layout):
579 self.edition_layout = CredEditor(self)
580 parent_layout.addLayout(self.edition_layout)
581
582 def setListItems(self):
583 self.listview.clear()
584 self.rootitem = CredRootItem(self.listview, self.title, self.model_object)
585 for obj in self.model_object.getCreds():
586 self.rootitem.addCred(obj)
587
588 def setEdition(self):
589 if self._current_item is not None:
590 if self._current_item.type == "Cred":
591 self.edition_layout.setCred(self._current_item.getModelObject())
592
593 def saveValue(self):
594 if self._current_item is not None:
595 if self._current_item.type == "Cred":
596 cred = self._current_item.getModelObject()
597 kwargs = self.edition_layout.getArgs()
598 if kwargs["username"] and kwargs["password"]:
599 guiapi.editCred(cred, **kwargs)
600 self.setListItems()
601
602 def addValue(self):
603 dialog = NewCredDialog(self, callback=self.__addValue)
604 dialog.exec_loop()
605
606 def __addValue(self, *args):
607 obj = self.rootitem.getModelObject()
608 guiapi.createAndAddCred(obj, *args)
609 self.setListItems()
610
611 def removeValue(self):
612 for item in self._selected_items:
613 if item.type == "Cred":
614 cred = item.getModelObject()
615 guiapi.delCred(cred.getParent().getID(), cred.getID())
616 self.setListItems()
617 self.edition_layout.clear()
618
619
620 def sizeHint(self):
621 return qt.QSize(750, 500)
622
623
624 class AboutDialog(BaseDialog):
625
626 def __init__(self, parent):
627 BaseDialog.__init__(self, parent, "AboutDialog",
628 layout_margin=15, layout_spacing=10, modal=True)
629 self.setCaption( 'About %s' % CONF.getAppname() )
630
631 self.logo = qt.QPixmap( os.path.join(CONF.getImagePath(),"about.png") )
632 self.logolabel = qt.QLabel( self )
633 self.logolabel.setPixmap( self.logo )
634 self.logolabel.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
635
636 self._about_text = u"""%s v%s""" % (CONF.getAppname(),CONF.getVersion())
637 self._about_text += "\nInfobyte LLC. All rights reserved"
638
639 self.text = qt.QLabel( self._about_text, self )
640 self.text.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
641
642 self.layout.addWidget( self.logolabel )
643 self.layout.addWidget( self.text )
644 self.setupButtons({"ok" : None})
645
646
647
648 class RepositoryConfigDialog(BaseDialog):
649
650 def __init__(self, parent, url="http://example:5984", replication = False, replics = "", callback=None):
651 BaseDialog.__init__(self, parent, "RepositoryConfig",
652 layout_margin=25, layout_spacing=20, modal=True)
653
654 self._callback = callback
655
656 self.setCaption("Repository Configuration")
657
658 hbox1 = qt.QHBox(self)
659 hbox1.setSpacing(10)
660 self._repourl_label = qt.QLabel("CouchDB (http://127.0.0.1:5984)", hbox1)
661 self._repourl_edit = qt.QLineEdit(hbox1)
662 if url: self._repourl_edit.setText(url)
663 self.layout.addWidget(hbox1)
664
665 hbox2 = qt.QHBox(self)
666 hbox2.setSpacing(5)
667 self._replicate_label = qt.QLabel("Replication enabled", hbox2)
668 self._replicate_edit = qt.QCheckBox(hbox2)
669 self._replicate_edit.setChecked(replication)
670
671 self.layout.addWidget(hbox2)
672
673 hbox3 = qt.QHBox(self)
674 hbox3.setSpacing(10)
675 self._replics_label = qt.QLabel("Replics", hbox3)
676 self.__replics_edit = qt.QLineEdit(hbox3)
677 if replics: self.__replics_edit.setText(replics)
678 self.layout.addWidget(hbox3)
679
680 self.__repourl_txt = self._repourl_edit.text()
681 self.__is_replicated_bool = self._replicate_edit.isChecked()
682 self.__replics_list_txt = self.__replics_edit.text()
683
684
685 self.setupButtons({ "ok" : self.ok_pressed,
686 "cancel" : None
687 })
688
689 def getData(self):
690 self.__repourl_txt = self._repourl_edit.text()
691 self.__is_replicated_bool = self._replicate_edit.isChecked()
692 self.__replics_list_txt = self.__replics_edit.text()
693 return (self.__repourl_txt.latin1(),
694 self.__is_replicated_bool,
695 self.__replics_list_txt.latin1())
696
697 def ok_pressed(self):
698 if self._callback is not None:
699 self._callback(*self.getData())
700 self.accept()
701
702
703
704 class ExceptionDialog(BaseDialog):
705
706 def __init__(self, parent, text="", callback=None, excection_objects=None):
707 BaseDialog.__init__(self, parent, "ExceptionDialog",
708 layout_margin=10, layout_spacing=15, modal=True)
709 self._callback = callback
710 self._excection_objects = excection_objects
711 self.setCaption('Error')
712
713 label1 = qt.QLabel("An unhandled error ocurred...", self )
714 label1.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
715 self.layout.addWidget(label1)
716
717 exception_textedit = qt.QTextEdit(self)
718 exception_textedit.setTextFormat(qt.Qt.LogText)
719 exception_textedit.append(text)
720 self.layout.addWidget(exception_textedit)
721
722 label2 = qt.QLabel("""Do you want to collect information and send it to Faraday developers?\n\
723 If you press Cancel the application will just continue.""", self )
724 label2.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
725 self.layout.addWidget(label2)
726
727 self.setupButtons({ "ok" : self.ok_pressed,
728 "cancel" : None
729 })
730 def ok_pressed(self):
731 if self._callback is not None:
732 self._callback(*self._excection_objects)
733 self.accept()
734
735 def sizeHint(self):
736 return qt.QSize(680, 300)
737
738
739 class SimpleDialog(BaseDialog):
740
741 def __init__(self, parent, text="", type="Information"):
742 BaseDialog.__init__(self, parent, "SimpleDialog",
743 layout_margin=10, layout_spacing=10, modal=True)
744 self.setCaption(type)
745
746
747 self.text = qt.QLabel(self)
748 self.text.setTextFormat(qt.Qt.RichText)
749 self.text.setText(text.replace("\n", "<br>"))
750 self.text.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
751 self.layout.addWidget( self.text )
752 self.setupButtons({"ok" : None})
753
754
755 class ExitDialog(BaseDialog):
756 def __init__(self, parent, callback=None,title="Exit", msg="Are you sure?"):
757 BaseDialog.__init__(self, parent, "ExitDialog",
758 layout_margin=20, layout_spacing=15, modal=True)
759 self.setCaption(title)
760
761 hbox1 = qt.QHBox(self)
762 hbox1.setSpacing(5)
763 self._message_label = qt.QLabel(msg, hbox1)
764 self._message_label.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
765 self.layout.addWidget(hbox1)
766 self.setupButtons({ "ok" : callback,
767 "cancel" : None
768 })
769
770 def sizeHint(self):
771 return qt.QSize(50, 50)
772
773
774
775 class MessageDialog(BaseDialog):
776 def __init__(self, parent, callback=None , title="Are you sure?", msg="Are you sure?", item=None):
777 BaseDialog.__init__(self, parent, "ExitDialog",
778 layout_margin=20, layout_spacing=15, modal=True)
779 self.setCaption(title)
780
781 self._callback = callback
782 self._item=item
783 hbox1 = qt.QHBox(self)
784 hbox1.setSpacing(5)
785 self._message_label = qt.QLabel(msg, hbox1)
786 self._message_label.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
787 self.layout.addWidget(hbox1)
788 self.setupButtons({ "ok" : self.ok_pressed,
789 "cancel" : None
790 })
791 def ok_pressed(self):
792 if self._callback is not None:
793 self._callback(self._item)
794 self.accept()
795
796 def sizeHint(self):
797 return qt.QSize(50, 50)
798
799
800
801 class VulnDialog(BaseDialog):
802
803 def __init__(self, parent, name="",description="", ref="", callback=None, item=None):
804 BaseDialog.__init__(self, parent, "VulnDialog",
805 layout_margin=10, layout_spacing=15, modal=True)
806
807 self._item = item
808 self._callback = callback
809 self.setCaption("New vulnerability" if name is "" else "Vuln %s" % item.name)
810
811
812
813 hbox1 = qt.QHBox(self)
814 hbox1.setSpacing(5)
815 name_label = qt.QLabel("Name", hbox1)
816 self._name_edit = qt.QLineEdit(hbox1)
817 if name: self._name_edit.setText(name)
818 self.layout.addWidget(hbox1)
819
820 hbox2 = qt.QHBox(self)
821 hbox2.setSpacing(5)
822 ref_label = qt.QLabel("Ref", hbox2)
823 self._ref_edit = qt.QLineEdit(hbox2)
824 if ref: self._ref_edit.setText(ref)
825 self.layout.addWidget(hbox2)
826
827 vbox6 = qt.QVBox(self)
828 vbox6.setSpacing(5)
829 description_label = qt.QLabel("Description:", vbox6 )
830
831 self._description_edit = qt.QTextEdit(vbox6)
832 self._description_edit.setTextFormat(qt.Qt.PlainText)
833 if description: self._description_edit.append(description)
834 self.layout.addWidget(vbox6)
835
836 self.setupButtons({ "ok" : self.ok_pressed,
837 "cancel" : None
838 })
839 def ok_pressed(self):
840 if self._callback is not None:
841 if self._name_edit.text() != "":
842 self._callback("%s" % self._name_edit.text(),"%s" % self._description_edit.text(),
843 "%s" % self._ref_edit.text(),self._item)
844 self.accept()
845 else:
846 dialog = SimpleDialog(self, "Please select a name")
847 dialog.exec_loop()
848
849 def sizeHint(self):
850 return qt.QSize(600, 400)
851
852
853 class CategoryDialog(BaseDialog):
854
855 def __init__(self, parent, name="", callback=None, item=None):
856 BaseDialog.__init__(self, parent, "CategoryDialog",
857 layout_margin=10, layout_spacing=15, modal=True)
858
859 self._item = item
860 self._callback = callback
861 self.setCaption("New category" if name is "" else "Category in %s" % item.name)
862
863 hbox1 = qt.QHBox(self)
864 hbox1.setSpacing(5)
865 name_label = qt.QLabel("Name", hbox1)
866 self._name_edit = qt.QLineEdit(hbox1)
867 if name: self._name_edit.setText(name)
868 self.layout.addWidget(hbox1)
869
870 self.setupButtons({ "ok" : self.ok_pressed,
871 "cancel" : None
872 })
873
874 def ok_pressed(self):
875 if self._callback is not None:
876 if self._name_edit.text() != "":
877 self._callback("%s" % self._name_edit.text(), self._item)
878 self.accept()
879 else:
880 dialog = SimpleDialog(self, "Please select a name")
881 dialog.exec_loop()
882
883 def sizeHint(self):
884 return qt.QSize(600, 400)
885
886
887
888 class NoteDialog(BaseDialog):
889
890 def __init__(self, parent, name="", text="", callback=None, item=None):
891 BaseDialog.__init__(self, parent, "NoteDialog",
892 layout_margin=10, layout_spacing=15, modal=True)
893
894 self._item = item
895 self._callback = callback
896 self.setCaption("New note" if name is "" else "Note %d" % item.id)
897
898 hbox1 = qt.QHBox(self)
899 hbox1.setSpacing(5)
900 name_label = qt.QLabel("Name", hbox1)
901 self._name_edit = qt.QLineEdit(hbox1)
902 if name: self._name_edit.setText(name)
903 self.layout.addWidget(hbox1)
904
905 vbox2 = qt.QVBox(self)
906 vbox2.setSpacing(3)
907 content_label = qt.QLabel("Note content:", vbox2 )
908
909 self._textedit = qt.QTextEdit(vbox2)
910 self._textedit.setTextFormat(qt.Qt.PlainText)
911 if text: self._textedit.append(text)
912 self.layout.addWidget(vbox2)
913
914 self.setupButtons({ "ok" : self.ok_pressed,
915 "cancel" : None
916 })
917 def ok_pressed(self):
918 if self._callback is not None:
919 if self._name_edit.text() != "":
920 if self._item is not None:
921 self._callback(self._name_edit.text(), self._textedit.text(),self._item)
922 else:
923 self._callback(self._name_edit.text(), self._textedit.text())
924 self.accept()
925 else:
926 dialog = SimpleDialog(self, "Please select a name")
927 dialog.exec_loop()
928
929 def sizeHint(self):
930 return qt.QSize(600, 400)
931
932
933
934
935 class NotificationWidget(qt.QLabel):
936 def __init__(self, parent, text=""):
937 qt.QLabel.__init__(self, parent, "notification")
938 pal = qt.QPalette()
939 color = qt.QColor(232, 226, 179, qt.QColor.Rgb)
940 pal.setColor(qt.QColorGroup.Background, color)
941 self.setTextFormat(qt.Qt.RichText)
942 self.setText(text.replace("\n", "<br>"))
943 self.setFrameStyle(qt.QFrame.PopupPanel | qt.QFrame.Plain)
944 self.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
945 self.setPalette(pal)
946
947 _w,_h=self._getsize(text)
948 self.resize(qt.QSize(_w,_h))
949
950 self._updatePos(parent)
951
952 def _getsize(self, text):
953 _tlist=text.split("\n")
954 _width=0
955 _w=10
956 for i in _tlist:
957 _size=len(i)
958 if _size > _width:
959 _width = _size
960 if _size > 80 and len(i.split(" ")) <=2:
961 _w=12
962
963
964 return _width*_w,(28*len(text.split("\n")))
965
966
967 def _updatePos(self, parent):
968 pos = qt.QPoint()
969 pos.setX(parent.width() - self.width() - 5)
970 pos.setY(parent.height() - self.height() - 20)
971 self.move(pos)
972
973 def closeNotification(self):
974 self.hide()
975 parent = self.parent()
976 parent.removeChild(self)
977 self.destroy()
978
979
980
981 class WorkspacePropertiesDialog(BaseDialog):
982
983 def __init__(self, parent, text="", callback=None, workspace=None):
984 BaseDialog.__init__(self, parent, "WorkspacePropertiesDialog",
985 layout_margin=10, layout_spacing=15, modal=True)
986 self._callback = callback
987 self.setCaption('Workspace Properties')
988
989
990 hbox1 = qt.QHBox(self)
991 hbox1.setSpacing(5)
992 self._name_label = qt.QLabel("Name", hbox1)
993 self._name_edit = qt.QLineEdit(hbox1)
994 self.layout.addWidget(hbox1)
995
996 hbox2 = qt.QHBox(self)
997 self._sdate_edit = qt.QDateEdit(hbox2, "start_date")
998 self.layout.addWidget(hbox2)
999
1000 hbox3 = qt.QHBox(self)
1001 self._fdate_edit = qt.QDateEdit(hbox3, "ftart_date")
1002 self.layout.addWidget(hbox3)
1003
1004 hbox4 = qt.QHBox(self)
1005 self._shared_checkbox = qt.QCheckBox("Shared", hbox4, "shared")
1006 self.layout.addWidget(hbox4)
1007
1008 hbox5 = qt.QHBox(self)
1009 hbox5.setSpacing(10)
1010 self._desc_label = qt.QLabel("Description", hbox5)
1011 self._desc_edit = qt.QTextEdit(hbox5)
1012 self.layout.addWidget(hbox5)
1013
1014
1015
1016 self.setupButtons({ "ok" : self.ok_pressed,
1017 "cancel" : None
1018 })
1019
1020 def ok_pressed(self):
1021
1022
1023 if self._callback is not None:
1024 name = self._name_edit.text()
1025 description = self._desc_edit.text()
1026 sdate = self._sdate_edit.date.toString()
1027 fdate = self._fdate_edit.date.toString()
1028 shared = self._shared_checkbox.checked
1029 self._callback()
1030 self.accept()
1031
1032 def sizeHint(self):
1033 return qt.QSize(600, 400)
1034
1035
1036
1037
1038 class WorkspaceCreationDialog(BaseDialog):
1039
1040 def __init__(self, parent, text="", callback=None, workspace=None, workspace_manager=None):
1041 BaseDialog.__init__(self, parent, "WorkspaceCreationDialog",
1042 layout_margin=10, layout_spacing=15, modal=True)
1043 self._callback = callback
1044 self.setCaption('New Workspace')
1045 self._main_window = parent
1046
1047 hbox1 = qt.QHBox(self)
1048 hbox1.setSpacing(5)
1049 self._name_label = qt.QLabel("Name", hbox1)
1050 self._name_edit = qt.QLineEdit(hbox1)
1051 self.layout.addWidget(hbox1)
1052
1053 hbox2 = qt.QHBox(self)
1054 hbox2.setSpacing(10)
1055 self._desc_label = qt.QLabel("Description", hbox2)
1056 self._desc_edit = qt.QTextEdit(hbox2)
1057 self.layout.addWidget(hbox2)
1058
1059 hbox3 = qt.QHBox(self)
1060 hbox3.setSpacing(10)
1061 self._type_label = qt.QLabel("Type", hbox3)
1062 self._type_combobox = qt.QComboBox(hbox3)
1063 self._type_combobox.setEditable(False)
1064 for w in workspace_manager.getAvailableWorkspaceTypes():
1065 self._type_combobox.insertItem(w)
1066 self.layout.addWidget(hbox3)
1067
1068 if len(workspace_manager.getAvailableWorkspaceTypes()) <= 1:
1069 parent.showPopup("No Couch Configuration available. Config, more workpsaces flavors")
1070
1071 self.__name_txt = self._name_edit.text()
1072 self.__desc_txt = self._desc_edit.text()
1073 self.__type_txt = str(self._type_combobox.currentText())
1074
1075 self.setupButtons({ "ok" : self.ok_pressed,
1076 "cancel" : None
1077 })
1078 def ok_pressed(self):
1079 res = re.match(r"^[a-z][a-z0-9\_\$()\+\-\/]*$", str(self._name_edit.text()))
1080 if res:
1081 if self._callback is not None:
1082 self.__name_txt = str(self._name_edit.text())
1083 self.__desc_txt = str(self._desc_edit.text())
1084 self.__type_txt = str(self._type_combobox.currentText())
1085 self._callback(self.__name_txt, self.__desc_txt, self.__type_txt)
1086 self.accept()
1087 else:
1088 self._main_window.showPopup("A workspace must be named with all lowercase letters (a-z), digits (0-9) or any of the _$()+-/ characters. The name has to start with a lowercase letter (a-z)")
1089
1090
1091
1092 class PluginSettingsDialog(BaseDialog, PluginSettingsUi):
1093 def __init__(self, parent=None, plugin_manager=None):
1094 BaseDialog.__init__(self, parent, "")
1095 PluginSettingsUi.__init__(self, parent)
1096
1097 self._plugin_manager = plugin_manager
1098 if plugin_manager is not None:
1099 self._plugin_settings = plugin_manager.getSettings()
1100 else:
1101 self._plugin_settings = {}
1102
1103 self._set_connections()
1104
1105 self._items = {}
1106 self._params = {}
1107
1108 self.t_parameters.horizontalHeader().setStretchEnabled(True, 0)
1109
1110 self._selected_plugin = None
1111 self._load_plugin_list()
1112
1113 def _set_connections(self):
1114 self.connect(self.lw_plugins, qt.SIGNAL("selectionChanged(QListViewItem*)"), self._show_plugin )
1115 self.connect(self.lw_plugins, qt.SIGNAL("clicked(QListViewItem*)"),
1116 self._show_plugin)
1117 self.connect(self.t_parameters, qt.SIGNAL("valueChanged(int, int)"),
1118 self._set_parameter)
1119 self.connect(self.bt_ok, qt.SIGNAL("clicked()"),
1120 self._update_settings)
1121
1122 def _load_plugin_list(self):
1123 if self._plugin_manager is None:
1124 return
1125
1126 for plugin_id, params in self._plugin_settings.iteritems():
1127 new_item = qt.QListViewItem(self.lw_plugins, "%s" % params["name"])
1128 self._items[new_item] = plugin_id
1129
1130 def _set_parameter(self, row, col):
1131 settings = self._plugin_settings[self._selected_plugin]["settings"]
1132 parameter = self.t_parameters.verticalHeader().label(row)
1133 value = self.t_parameters.text(row, col)
1134 settings[str(parameter).strip()] = str(value).strip()
1135
1136 def _update_settings(self):
1137 if self._plugin_manager is not None:
1138 self._plugin_manager.updateSettings(self._plugin_settings)
1139
1140 def _show_plugin(self, item):
1141 if item is None:
1142 return
1143
1144 self.t_parameters.removeRows(range(self.t_parameters.numRows()))
1145
1146 plugin_id = self._items[item]
1147 self._selected_plugin = plugin_id
1148
1149 params = self._plugin_settings[plugin_id]
1150
1151 self.le_name.setText(params["name"])
1152 self.le_version.setText(params["version"])
1153 self.le_pversion.setText(params["plugin_version"])
1154
1155 for setting, value in params["settings"].iteritems():
1156 index = self.t_parameters.numRows()
1157 self.t_parameters.insertRows(index)
1158
1159 self.t_parameters.verticalHeader().setLabel(index, setting)
1160 self.t_parameters.setText(index, 0, str(value))
1161
1162
1163
1164 class VulnsListDialog(BaseDialog, VulnerabilitiesUi):
1165 def __init__(self, parent=None,item=None):
1166 BaseDialog.__init__(self, parent, "")
1167 VulnerabilitiesUi.__init__(self, parent)
1168 self._vulns = []
1169 self._setup_signals()
1170 self._item=item
1171
1172 self.t_vulns.setColumnReadOnly(0, True)
1173 self.t_vulns.setColumnReadOnly(1, True)
1174 self.t_vulns.setColumnReadOnly(2, True)
1175
1176 self.t_vulns.horizontalHeader().setStretchEnabled(True, 3)
1177
1178 def add_vuln(self, vuln):
1179 index = self.t_vulns.numRows()
1180 self._vulns.append(vuln)
1181 self.t_vulns.insertRows(index)
1182
1183 self.t_vulns.setText(index, 0, str(vuln.name))
1184 self.t_vulns.setText(index, 1, str(vuln.refs))
1185 self.t_vulns.setText(index, 2, str(vuln.desc))
1186
1187 self.t_vulns.adjustColumn(0)
1188 self.t_vulns.adjustColumn(1)
1189 self.t_vulns.adjustColumn(2)
1190
1191
1192 def del_vuln(self, vuln):
1193
1194 index = self.t_vulns.currentRow()
1195 self._vulns.remove(vuln)
1196 self.t_vulns.removeRows([index])
1197
1198
1199
1200 def _setup_signals(self):
1201
1202 self.connect(self.t_vulns, SIGNAL("doubleClicked(int,int,int,QPoint)"),self._edit)
1203
1204 self.connect(self.add_button, SIGNAL("clicked()"), self._add)
1205 self.connect(self.edit_button, SIGNAL("clicked()"), self._edit)
1206 self.connect(self.delete_button, SIGNAL("clicked()"), self._delete)
1207 self.connect(self.list_note_button, SIGNAL("clicked()"), self._list_note)
1208 self.connect(self.manage_evidence_button, SIGNAL("clicked()"), self._evidence)
1209
1210
1211 def _edit(self):
1212
1213 if self.t_vulns.currentSelection() != -1:
1214 _object=self._vulns[self.t_vulns.currentRow()]
1215 dialog = VulnDialog(self,str(_object.name),str(_object.desc),str(_object.refs),self._editcallback,_object)
1216 res = dialog.exec_loop()
1217
1218 def _evidence(self):
1219
1220 if self.t_vulns.currentSelection() != -1:
1221 _object=self._vulns[self.t_vulns.currentRow()]
1222 _object.object = _object
1223 dialog = EvidencesListDialog(self, _object)
1224
1225
1226 dialog.exec_loop()
1227
1228 def _list_note(self):
1229
1230 if self.t_vulns.currentSelection() != -1:
1231 _object=self._vulns[self.t_vulns.currentRow()]
1232 _object.object = _object
1233 dialog = NotesListDialog(self, _object)
1234
1235
1236 dialog.exec_loop()
1237
1238 def _editcallback(self,name,desc,ref,item):
1239
1240 item.name=name
1241 item.desc=desc
1242 item.ref=ref
1243
1244 self.t_vulns.setText(self.t_vulns.currentRow(), 0, name)
1245 self.t_vulns.setText(self.t_vulns.currentRow(), 1, ref)
1246 self.t_vulns.setText(self.t_vulns.currentRow(), 2, desc)
1247
1248 def _newcallback(self, name, desc, ref, item):
1249 _parent=self._item.object.getParent()
1250
1251 api.devlog("newVuln (%s) (%s) (%s) (%s) " % (name, desc, ref, item.object.getName(),))
1252
1253 _newvuln=api.newVuln(name,desc,ref)
1254
1255 if item.type == "Application":
1256 api.addVulnToApplication(_newvuln,_parent.name,item.object.getName())
1257 elif item.type == "Interface":
1258 api.addVulnToInterface(_newvuln,_parent.name,item.object.getName())
1259 elif item.type == "Host":
1260 api.addVulnToHost(_newvuln,item.object.getName())
1261 elif item.type == "Service":
1262 api.addVulnToService(_newvuln,_parent.name,item.object.getName())
1263
1264
1265 self.add_vuln(_newvuln)
1266
1267 def _add(self):
1268 if self._item is not None and self._item.object is not None:
1269 dialog = VulnDialog(self,callback=self._newcallback,item=self._item)
1270 res = dialog.exec_loop()
1271
1272
1273 def _delete(self):
1274 if self.t_vulns.currentSelection() != -1:
1275 _vuln=self._vulns[self.t_vulns.currentRow()]
1276 _parent=_vuln._parent
1277
1278
1279 if isinstance(_parent,hosts.HostApplication):
1280 api.delVulnFromApplication(_vuln.getID(),_parent.getParent().name,_parent.name)
1281 elif isinstance(_parent,hosts.Interface):
1282 api.delVulnFromInterface(_vuln.getID(),_parent.getParent().name,_parent.name)
1283 elif isinstance(_parent,hosts.Host):
1284 api.delVulnFromHost(_vuln.getID(),_parent.name)
1285 elif isinstance(_parent,hosts.Service):
1286 api.delVulnFromService(_vuln.getID(),_parent.getParent().name,_parent.name)
1287
1288
1289 self.del_vuln(_vuln)
1290
1291
1292
1293
1294 class PreferencesDialog(BaseDialog, PreferencesUi):
1295 def __init__(self, parent=None):
1296 BaseDialog.__init__(self, parent, "")
1297 PreferencesUi.__init__(self, parent)
1298 self._main_window = parent
1299
1300 self._fdb = qt.QFontDatabase()
1301 self._families = self._fdb.families()
1302 self.cb_font_family.insertStringList(self._families)
1303
1304 self._styles = None
1305 self._sizes = None
1306
1307 self._family = None
1308 self._style = None
1309 self._size = None
1310
1311
1312 self._set_connections()
1313 self._load_styles(0)
1314 self._load_sizes(0)
1315
1316 def _set_connections(self):
1317 self.connect(self.cb_font_family, SIGNAL("activated(int)"),
1318 self._load_styles)
1319 self.connect(self.cb_font_style, SIGNAL("activated(int)"),
1320 self._load_sizes)
1321 self.connect(self.cb_font_size, SIGNAL("activated(int)"),
1322 self._change_size)
1323 self.connect(self.bt_ok, SIGNAL("clicked()"),
1324 self.accept)
1325 self.connect(self.bt_cancel, SIGNAL("clicked()"),
1326 self.reject)
1327
1328 def _load_styles(self, index):
1329 self._family = self._families[index]
1330 self.cb_font_style.clear()
1331 self._styles = self._fdb.styles(self._family)
1332 self.cb_font_style.insertStringList(self._styles)
1333 self._update_font()
1334
1335 def _load_sizes(self, index):
1336 self._style = self._styles[index]
1337 self.cb_font_size.clear()
1338 self._sizes = self._fdb.smoothSizes(self._family, self._style)
1339 string_list = QStringList()
1340 [string_list.append(str(size)) for size in self._sizes]
1341 self.cb_font_size.insertStringList(string_list)
1342 self._update_font()
1343
1344 def _change_size(self, index):
1345 self._size = self._sizes[index]
1346 self._update_font()
1347
1348 def _update_font(self):
1349 font = self.le_example.font()
1350 if self._family is not None:
1351 font.setFamily(self._family)
1352 if self._size is not None:
1353 font.setPointSize(self._size)
1354 if self._style is not None:
1355 isItalic = self._fdb.italic(self._family, self._style)
1356 font.setItalic(isItalic)
1357 isBold = self._fdb.bold(self._family, self._style)
1358 font.setBold(isBold)
1359 weight = self._fdb.weight(self._family, self._style)
1360 font.setWeight(weight)
1361 self.le_example.setFont(font)
1362 self._main_window.shell_font = font
1363
1364
1365
1366 class NotesListDialog(BaseDialog, NotesListUI):
1367
1368 def __init__(self, parent, item=None):
1369 BaseDialog.__init__(self, parent, "NotesListDialog", modal=True)
1370 NotesListUI.__init__(self, parent)
1371 self.notes_table.setColumnReadOnly(0, True)
1372 self.notes_table.setColumnReadOnly(1, True)
1373 self._notes = []
1374 self._setup_signals()
1375 self._item = item
1376 if item is not None and item.object is not None:
1377 for n in item.object.getNotes():
1378 self.add_note_to_table(n)
1379
1380 def add_note_to_table(self, note):
1381 index = self.notes_table.numRows()
1382 self._notes.append(note)
1383 self.notes_table.insertRows(index)
1384 self.notes_table.setText(index, 0, note.name)
1385 self.notes_table.setText(index, 1, note.text)
1386 self.notes_table.adjustColumn(0)
1387 self.notes_table.adjustColumn(1)
1388 self.notes_table.adjustRow(index)
1389
1390 def _setup_signals(self):
1391
1392
1393
1394
1395 self.connect(self.add_button, SIGNAL("clicked()"), self._add_note)
1396 self.connect(self.edit_button, SIGNAL("clicked()"), self._edit_note)
1397 self.connect(self.delete_button, SIGNAL("clicked()"), self._delete_note)
1398 self.connect(self.list_note_button, SIGNAL("clicked()"), self._list_note)
1399
1400 def _edit_note(self):
1401 if self.notes_table.currentSelection() != -1:
1402 _object=self._notes[self.notes_table.currentRow()]
1403 dialog = NoteDialog(self,_object.name,_object.text,self._editcallbackNote,_object)
1404 res = dialog.exec_loop()
1405
1406 def _list_note(self):
1407
1408 if self.notes_table.currentSelection() != -1:
1409 _object=self._notes[self.notes_table.currentRow()]
1410 _object.object = _object
1411 dialog = NotesListDialog(self, _object)
1412
1413
1414 dialog.exec_loop()
1415
1416 def _editcallbackNote(self,name,text,item):
1417 item.name = name
1418 item.text = text
1419 self.notes_table.setText(self.notes_table.currentRow(), 0, name)
1420 self.notes_table.setText(self.notes_table.currentRow(), 1, text)
1421
1422 def _newcallbackNote(self, name, text, item):
1423 _parent=self._item.object.getParent()
1424
1425 api.devlog("newNote (%s) (%s) (%s) " % (name, text, item.object.getName(),))
1426
1427 _newnote=api.newNote(name,text)
1428
1429 if item.object.class_signature == "HostApplication":
1430 api.addNoteToApplication(_newnote,_parent.name,item.object.getName())
1431 elif item.object.class_signature == "Interface":
1432 api.addNoteToInterface(_newnote,_parent.name,item.object.getName())
1433 elif item.object.class_signature == "Host":
1434 api.addNoteToHost(_newnote,item.object.getName())
1435 elif item.object.class_signature == "Service":
1436 api.addNoteToService(_newnote,_parent.name,item.object.getName())
1437 else:
1438
1439
1440 item.object.addNote(_newnote)
1441
1442 self.add_note_to_table(_newnote)
1443
1444 def _add_note(self):
1445 if self._item is not None and self._item.object is not None:
1446 dialog = NoteDialog(self,callback=self._newcallbackNote,item=self._item)
1447 res = dialog.exec_loop()
1448
1449
1450 def _delete_note(self):
1451
1452 _object=self._notes[self.notes_table.currentRow()]
1453 if self.notes_table.currentSelection() != -1:
1454 _note=self._notes[self.notes_table.currentRow()]
1455 _parent=_note._parent
1456
1457 if _parent.class_signature == "HostApplication":
1458 api.delNoteFromApplication(_note.getID(),_parent.getParent().name,_parent.name)
1459 elif _parent.class_signature == "Interface":
1460 api.delNoteFromInterface(_note.getID(),_parent.getParent().name,_parent.name)
1461 elif _parent.class_signature == "Host":
1462 api.delNoteFromHost(_note.getID(),_parent.name)
1463 elif _parent.class_signature == "Service":
1464 api.delNoteFromService(_note.getID(),_parent.getParent().name,_parent.name)
1465 else:
1466 _parent.delNote(_note.getID())
1467
1468
1469
1470 self.del_note(_note)
1471
1472
1473 def del_note(self, note):
1474
1475 index = self.notes_table.currentRow()
1476 self._notes.remove(note)
1477 self.notes_table.removeRows([index])
1478
1479
1480
1481
1482
1483 class EvidencesListDialog(BaseDialog, EvidencesListUI):
1484
1485 def __init__(self, parent, item=None):
1486 BaseDialog.__init__(self, parent, "EvidencesListDialog", modal=True)
1487 EvidencesListUI.__init__(self, parent)
1488 self.evidences_table.setColumnReadOnly(0, True)
1489 self.evidences_table.setColumnReadOnly(1, True)
1490 self._setup_signals()
1491 self._item = item
1492 if item is not None and item.object is not None:
1493 for n in item.object.evidences:
1494 self.add_evidence_to_table(n)
1495
1496 def add_evidence_to_table(self, evidence):
1497 index = self.evidences_table.numRows()
1498 self.evidences_table.insertRows(index)
1499 self.evidences_table.setText(index, 0, str(index))
1500 self.evidences_table.setText(index, 1, evidence)
1501 self.evidences_table.adjustColumn(0)
1502 self.evidences_table.adjustColumn(1)
1503 self.evidences_table.adjustRow(index)
1504
1505 def _setup_signals(self):
1506
1507
1508
1509
1510 self.connect(self.add_button, SIGNAL("clicked()"), self._add_evidence)
1511 self.connect(self.delete_button, SIGNAL("clicked()"), self._delete_evidence)
1512
1513 def _newcallbackEvidence(self, name, item):
1514
1515 d_path = api.addEvidence("%s" % name)
1516 if d_path is not False:
1517 self._item.object.evidences.append(d_path)
1518 self.add_evidence_to_table(d_path)
1519
1520 def _add_evidence(self):
1521 if self._item is not None and self._item.object is not None:
1522 filename = QFileDialog.getOpenFileName(
1523 CONF.getDefaultTempPath(),
1524 "Images Files (*.png)",
1525 None,
1526 "open file dialog",
1527 "Choose a file to add in the evidence" );
1528
1529 if (filename):
1530 self._newcallbackEvidence(filename,self._item)
1531 for n in self._item.object.evidences:
1532 api.devlog("Los items screenshot son:" + n)
1533
1534
1535 def _delete_evidence(self):
1536
1537 if self.evidences_table.currentSelection() != -1:
1538
1539 index = self.evidences_table.currentRow()
1540 _evidence=self._item.object.evidences[index]
1541 self._item.object.evidences.remove(_evidence)
1542 api.delEvidence(_evidence)
1543 self.evidences_table.removeRows([index])
1544 self._updateIds()
1545 for n in self._item.object.evidences:
1546 api.devlog("Los items screenshot son:" + n)
1547
1548 def _updateIds(self):
1549 for i in range(0,self.evidences_table.numRows()):
1550 self.evidences_table.setText(i , 0, str(i))
1551
1552
1553
1554
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import os
9 import qt
10 import model.api as api
11 import model.guiapi as guiapi
12 import re
13 import model.hosts as hosts
14 from managers.model_managers import WorkspaceManager
15 from ui.plugin_settings import *
16 from ui.vulnerabilities import *
17 from ui.preferences import *
18 from ui.noteslist import NotesListUI
19 from ui.evidenceslist import *
20 from edition import EditionTable, HostEditor, ServiceEditor, InterfaceEditor, NoteEditor, NewNoteDialog, VulnEditor, NewVulnDialog, VulnWebEditor, NewCredDialog, CredEditor
21 from modelobjectitems import NoteRootItem, VulnRootItem, CredRootItem
22
23 from config.configuration import getInstanceConfiguration
24 CONF = getInstanceConfiguration()
25
26
27 class BaseDialog(qt.QDialog):
28
29 def __init__(self, parent, name, layout_margin=0, layout_spacing=-1, modal=True):
30 qt.QDialog.__init__(self, parent, name, modal)
31 if layout_spacing == -1:
32 layout_spacing = self.fontMetrics().height()
33
34 self.layout = qt.QVBoxLayout(self, layout_margin, layout_spacing, "layout")
35 self.button_box = None
36 self.ok_button = None
37 self.cancel_button = None
38 self.quit_button = None
39
40 def setupButtons(self, buttons=None):
41 """
42 Creates and setup buttons clicked singal connection using callbacks provided
43 The buttons parameter must be a dict with keys (in lowercase)
44 "ok", "cancel" or "quit" and a callback reference as value.
45 If None is provided as callback value then default behaviour will
46 be applied to the button (accept for Ok, reject for cancel and quit)
47 Button order will be always Ok, Cancel and Quit
48 This will add only the buttons provided in the buttons parameter, so not all
49 keys must be used. You can add only the ones needed.
50 If no parameter is provided, OK and Cancel buttons will be added with
51 their default behaviour.
52 IMPORTANT: if callbacks do not call accept or reject methods, then
53 the dialog won't end and will be visible. Remember to call accept &
54 reject internally on your provided callbacks
55 """
56 self.button_box = qt.QHBoxLayout(self.layout)
57 spacer = qt.QSpacerItem(0,0,qt.QSizePolicy.Expanding,qt.QSizePolicy.Minimum)
58 self.button_box.addItem(spacer)
59
60 if buttons is None:
61 self._addOkButton()
62 self._addCancelButton()
63 else:
64 if "ok" in buttons:
65 self._addOkButton(buttons["ok"])
66 if "cancel" in buttons:
67 self._addCancelButton(buttons["cancel"])
68 if "quit" in buttons:
69 self._addQuitButton(buttons["quit"])
70
71 def _addOkButton(self, callback=None):
72 self.ok_button = qt.QPushButton( "OK", self )
73 self.button_box.addWidget( self.ok_button )
74 if callback is None:
75 callback = self.accept
76 self.connect( self.ok_button, qt.SIGNAL('clicked()'), callback )
77
78 def _addCancelButton(self, callback=None):
79 self.cancel_button = qt.QPushButton("Cancel", self)
80 self.button_box.addWidget( self.cancel_button )
81 if callback is None:
82 callback = self.reject
83 self.connect( self.cancel_button, qt.SIGNAL('clicked()'), callback)
84
85 def _addQuitButton(self, callback=None):
86 self.quit_button = qt.QPushButton("Quit", self)
87 self.button_box.addWidget( self.quit_button )
88 if callback is None:
89 callback = self.reject
90 self.connect( self.quit_button, qt.SIGNAL('clicked()'), callback)
91
92 def sizeHint(self):
93
94 return qt.QSize(400, 150)
95
96
97
98 class LoginDialog(BaseDialog):
99 def __init__(self, parent, callback):
100 BaseDialog.__init__(self, parent, "Login",
101 layout_margin=10, layout_spacing=15, modal=True)
102
103 self._auth_callback = callback
104 self.setCaption("Login")
105
106 hbox1 = qt.QHBox(self)
107 hbox1.setSpacing(5)
108 self._username_label = qt.QLabel("Username", hbox1)
109 self._username_edit = qt.QLineEdit(hbox1)
110 self.layout.addWidget(hbox1)
111
112 hbox2 = qt.QHBox(self)
113 hbox2.setSpacing(10)
114 self._password_label = qt.QLabel("Password", hbox2)
115 self.__password_edit = qt.QLineEdit(hbox2)
116 self.__password_edit.setEchoMode(qt.QLineEdit.Password)
117 self.layout.addWidget(hbox2)
118
119 self.__username_txt = self._username_edit.text()
120 self.__passwd_txt = self.__password_edit.text()
121
122 self.setupButtons({
123 "ok" : self._login,
124 "cancel" : self._clear,
125 "quit" : None,
126 })
127
128 def getData(self):
129 self.__username_txt = self._username_edit.text()
130 self.__passwd_txt = self.__password_edit.text()
131 return self.__username_txt.latin1(), self.__passwd_txt.latin1()
132
133 def _login(self):
134
135
136 self.__username_txt = self._username_edit.text()
137 self.__passwd_txt = self.__password_edit.text()
138 api.devlog("Username: %s\nPassword: %s" %(self.__username_txt, self.__passwd_txt))
139 self.accept()
140
141 def _clear(self):
142
143 self._username_edit.clear()
144 self.__password_edit.clear()
145
146 def sizeHint(self):
147 return qt.QSize(250, 100)
148
149
150
151 class DebugPersistenceDialog(BaseDialog):
152
153 def __init__(self, parent):
154 BaseDialog.__init__(self, parent, "PersistenceDebugDialog",
155 layout_margin=15, layout_spacing=10, modal=True)
156 self.setCaption( 'Persistence Debug Dialog' )
157
158 self.layout.addWidget( self.logolabel )
159 self.layout.addWidget( self.text )
160 self.setupButtons({"ok" : None})
161
162
163
164 class ConflictResolutionDialog(BaseDialog):
165 def __init__(self, conflicts, parent=None, name=None):
166
167 BaseDialog.__init__(self, parent, "Conflicts",
168 layout_margin=10, layout_spacing=15, modal=True)
169
170 self.conflict = None
171 self.conflict_iterator = iter(conflicts)
172 self.first_object = None
173 self.second_object = None
174
175 hbox = qt.QHBoxLayout()
176
177 vbox = qt.QVBoxLayout()
178 self.label_first_object = qt.QLabel("", self)
179 vbox.addWidget(self.label_first_object)
180 self.detailtable_first_object = EditionTable(self)
181 self.editor_first_object = None
182 vbox.addWidget(self.detailtable_first_object)
183 self.choice_button_first_object = qt.QRadioButton(self, "")
184 vbox.addWidget(self.choice_button_first_object)
185
186 hbox.addLayout(vbox)
187
188 vbox = qt.QVBoxLayout()
189 self.label_second_object = qt.QLabel("", self)
190 vbox.addWidget(self.label_second_object)
191 self.detailtable_second_object = EditionTable(self)
192 self.editor_second_object = None
193 vbox.addWidget(self.detailtable_second_object)
194 self.choice_button_second_object = qt.QRadioButton(self, "")
195 vbox.addWidget(self.choice_button_second_object)
196
197 self.object_group_button = qt.QButtonGroup()
198 self.object_group_button.insert(self.choice_button_first_object)
199 self.object_group_button.insert(self.choice_button_second_object)
200
201 hbox.addLayout(vbox)
202
203 self.layout.addLayout(hbox)
204
205 self.setupButtons({"ok": self.resolve, "cancel": self.quit})
206
207 self.del_callback = None
208 self.add_callback = None
209
210 self.setup()
211
212 def setup(self):
213 self.getNextConflict()
214 if self.conflict:
215 self.setupConflict()
216 else:
217 self.accept()
218
219 def getNextConflict(self):
220 try:
221 self.conflict = self.conflict_iterator.next()
222 except StopIteration:
223 self.conflict = None
224
225 def setupConflict(self):
226 if not self.conflict:
227 return
228
229 self.first_object = self.conflict.getFirstObject()
230 self.second_object = self.conflict.getSecondObject()
231 type = self.conflict.getModelObjectType()
232
233 self.setCaption(type)
234 name_first_object = self.first_object.getName()
235 name_second_object = self.second_object.getName()
236 if self.first_object.getParent() is not None:
237 name_first_object += " (Host: %s)" % self.first_object.getHost().getName()
238 name_second_object += " (Host: %s)" % self.first_object.getHost().getName()
239 self.label_first_object.setText(name_first_object)
240 self.label_second_object.setText(name_second_object)
241
242 if type == "Host":
243 self.editor_first_object = HostEditor(self.first_object)
244 self.editor_second_object = HostEditor(self.second_object)
245 elif type == "Interface":
246 self.editor_first_object = InterfaceEditor(self.first_object)
247 self.editor_second_object = InterfaceEditor(self.second_object)
248 elif type == "Service":
249 self.editor_first_object = ServiceEditor(self.first_object)
250 self.editor_second_object = ServiceEditor(self.second_object)
251
252 self.editor_first_object.fillEditionTable(self.detailtable_first_object)
253
254 self.editor_second_object.fillEditionTable(self.detailtable_second_object)
255
256
257 def getSelectedEditor(self):
258 if self.choice_button_first_object.isChecked():
259 editor = self.editor_first_object
260 elif self.choice_button_second_object.isChecked():
261 editor = self.editor_second_object
262 else:
263 editor = None
264 return editor
265
266 def resolve(self):
267 editor_selected = self.getSelectedEditor()
268 if editor_selected:
269 guiapi.resolveConflict(self.conflict, editor_selected.getArgs())
270 self.setup()
271
272 def quit(self):
273 self.reject()
274
275 def sizeHint(self):
276 return qt.QSize(750, 500)
277
278
279
280 class ModelObjectListViewItem(qt.QListViewItem):
281 def __init__(self, qtparent, model_object=None):
282 qt.QListViewItem.__init__(self, qtparent)
283 self.model_object = model_object
284 if self.model_object:
285 self.setText(0, model_object.name)
286 else:
287 self.setText(0, "")
288
289 def getModelObject(self):
290 return self.model_object
291
292 class ListableObjecttDialog(BaseDialog):
293 def __init__(self, parent=None, title=None, model_object=None, objects_list = [], layout_margin=10, layout_spacing=15, modal=True):
294 BaseDialog.__init__(self, parent, title,
295 layout_margin=10, layout_spacing=15, modal=True)
296
297 hbox = qt.QHBoxLayout()
298 vbox1 = qt.QVBoxLayout()
299 vbox1.setMargin(5)
300 vbox2 = qt.QVBoxLayout()
301 vbox2.setMargin(5)
302 self.model_object = model_object
303 self.objects_list = objects_list
304 self._selected_object = None
305 self._current_item = None
306 self._selected_items = []
307 self.edition_layout = None
308 self.title = title
309
310 self.listview = qt.QListView(self)
311 self.listview.setSorting(-1)
312 self.listview.setSelectionMode(qt.QListView.Extended)
313 self.connect(self.listview, qt.SIGNAL("selectionChanged()"), self._itemSelected)
314 self.listview.addColumn(title, self.listview.size().width())
315 self.listview.setColumnWidthMode(0, qt.QListView.Maximum)
316 self.setListItems()
317
318 vbox1.addWidget(self.listview)
319
320 self.button_box1 = qt.QHBoxLayout()
321 self.add_button = qt.QPushButton("Add", self)
322 self.add_button.setMaximumSize(qt.QSize(100, 25))
323 self.connect(self.add_button, qt.SIGNAL('clicked()'), self.addValue)
324 self.remove_button = qt.QPushButton("Remove", self)
325 self.remove_button.setMaximumSize(qt.QSize(100, 25))
326 self.connect(self.remove_button, qt.SIGNAL('clicked()'), self.removeValue)
327 self.button_box1.addWidget(self.add_button)
328 self.button_box1.addWidget(self.remove_button)
329 self.button_box1.addStretch(1)
330
331 vbox1.addLayout(self.button_box1)
332
333 self.setupEditor(vbox2)
334
335 self.button_box2 = qt.QHBoxLayout()
336 self.save_button = qt.QPushButton("Save", self)
337 self.save_button.setMaximumSize(qt.QSize(100, 25))
338 self.connect(self.save_button, qt.SIGNAL('clicked()'), self.saveValue)
339 self.button_box2.addWidget(self.save_button)
340 self.button_box2.addStretch(1)
341
342 vbox2.addLayout(self.button_box2)
343
344 hbox.setSpacing(6)
345 hbox.addLayout(vbox1)
346 hbox.addLayout(vbox2)
347 self.layout.addLayout(hbox)
348
349 self.setupButtons({"quit": None})
350
351 def setupEditor(self, parent_layout):
352 pass
353
354 def saveValue(self):
355 pass
356
357 def addValue(self):
358 pass
359
360 def removeValue(self):
361 pass
362
363 def _itemSelected(self):
364 self.edition_layout.clear()
365 self._current_item = self.listview.currentItem()
366 i = self.listview.firstChild()
367 self._selected_items=[]
368 while i is not None:
369 if i.isSelected():
370 self._selected_items.append(i)
371 i = i.itemBelow()
372 self.setEdition()
373
374 def sizeHint(self):
375 return qt.QSize(750, 500)
376
377 class NotesDialog(ListableObjecttDialog):
378 def __init__(self, parent=None, model_object=None):
379 ListableObjecttDialog.__init__(self, parent, "Notes", model_object, model_object.getNotes(),
380 layout_margin=10, layout_spacing=15, modal=True)
381
382 def setupEditor(self, parent_layout):
383 self.edition_layout = NoteEditor(self)
384 parent_layout.addLayout(self.edition_layout)
385
386 def setListItems(self):
387 self.listview.clear()
388 self.rootitem = NoteRootItem(self.listview, self.title, self.model_object)
389 for obj in self.model_object.getNotes():
390 self.rootitem.addNote(obj)
391
392 def setEdition(self):
393 if self._current_item is not None:
394 if self._current_item.type == "Note":
395 self.edition_layout.setNote(self._current_item.getModelObject())
396
397 def saveValue(self):
398 if self._current_item is not None:
399 if self._current_item.type == "Note":
400 note = self._current_item.getModelObject()
401 kwargs = self.edition_layout.getArgs()
402 if kwargs["name"] and kwargs["text"]:
403 guiapi.editNote(note, **kwargs)
404 self.setListItems()
405
406 def addValue(self):
407 dialog = NewNoteDialog(self, callback=self.__addValue)
408 dialog.exec_loop()
409
410 def __addValue(self, *args):
411 obj = self.rootitem.getModelObject()
412 if self._current_item:
413 obj = self._current_item.getModelObject()
414 guiapi.createAndAddNote(obj, *args)
415 self.setListItems()
416
417 def removeValue(self):
418 for item in self._selected_items:
419 if item.type == "Note":
420 note = item.getModelObject()
421 guiapi.delNote(note.getParent().getID(), note.getID())
422 self.setListItems()
423 self.edition_layout.clear()
424
425
426 def sizeHint(self):
427 return qt.QSize(750, 500)
428
429
430 class VulnsDialog(ListableObjecttDialog):
431 def __init__(self, parent=None, model_object=None):
432 ListableObjecttDialog.__init__(self, parent, "Vulns", model_object, model_object.getVulns(),
433 layout_margin=10, layout_spacing=15, modal=True)
434
435 def setupEditor(self, parent_layout):
436 self._widget_stack = qt.QWidgetStack(self)
437
438 self._vuln_edition_widget = qt.QFrame()
439 self._vuln_edition_layout = VulnEditor(self._vuln_edition_widget)
440
441 self._vuln_web_scrollbar_view = qt.QScrollView()
442 self._vuln_web_scrollbar_view.setResizePolicy(qt.QScrollView.AutoOneFit)
443 self._vuln_web_edition_widget = qt.QFrame(self._vuln_web_scrollbar_view.viewport())
444 self._vuln_web_edition_layout = VulnWebEditor(self._vuln_web_edition_widget)
445 self._vuln_web_edition_layout.setMargin(5)
446 self._vuln_web_scrollbar_view.addChild(self._vuln_web_edition_widget)
447
448 self._widget_stack.addWidget(self._vuln_edition_widget, 0)
449 self._widget_stack.addWidget(self._vuln_web_scrollbar_view, 1)
450 self._widget_stack.raiseWidget(self._vuln_edition_widget)
451
452 self._vuln_edition_widget.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Ignored))
453 self._vuln_web_edition_widget.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Ignored))
454
455 self.edition_layout = self._vuln_edition_widget.layout()
456
457 parent_layout.addWidget(self._widget_stack)
458
459 def setListItems(self):
460 self.listview.clear()
461 self.rootitem = VulnRootItem(self.listview, self.title, self.model_object)
462 for obj in self.model_object.getVulns():
463 self.rootitem.addVuln(obj)
464
465 def setEdition(self):
466 if self._current_item is not None:
467 if self._current_item.type == "Vuln" or self._current_item.type == "VulnWeb":
468 widget = self._vuln_edition_widget
469 self._widget_stack.raiseWidget(widget)
470 if self._current_item.type == "VulnWeb":
471 widget = self._vuln_web_edition_widget
472 self._widget_stack.raiseWidget(self._vuln_web_scrollbar_view)
473 self.edition_layout = widget.layout()
474 self.edition_layout.setVuln(self._current_item.getModelObject())
475
476 def saveValue(self):
477 if self._current_item is not None:
478 if self._current_item.type == "Vuln" or self._current_item.type == "VulnWeb":
479 vuln = self._current_item.getModelObject()
480 kwargs = self.edition_layout.getArgs()
481 if kwargs["name"] and kwargs["desc"]:
482 if self._current_item.type == "Vuln":
483 guiapi.editVuln(vuln, **kwargs)
484 else:
485 guiapi.editVulnWeb(vuln, **kwargs)
486 self.setListItems()
487
488 def addValue(self):
489 vuln_web_enabled = False
490 if self.model_object.class_signature == "Service":
491 vuln_web_enabled = True
492 dialog = NewVulnDialog(self, callback=self.__addValue, vuln_web_enabled=vuln_web_enabled)
493 dialog.exec_loop()
494
495 def __addValue(self, *args):
496 obj = self.model_object
497 if args[0]:
498
499 guiapi.createAndAddVulnWeb(obj, *args[1:])
500 else:
501 guiapi.createAndAddVuln(obj, *args[1:])
502 self.setListItems()
503
504 def removeValue(self):
505 for item in self._selected_items:
506 if item.type == "Vuln" or item.type == "VulnWeb":
507 vuln = item.getModelObject()
508 guiapi.delVuln(vuln.getParent().getID(), vuln.getID())
509 self.setListItems()
510 self.edition_layout.clear()
511
512 def sizeHint(self):
513 return qt.QSize(850, 500)
514
515
516 class CredsDialog(ListableObjecttDialog):
517 def __init__(self, parent=None, model_object=None):
518 ListableObjecttDialog.__init__(self, parent, "Credentials", model_object, model_object.getCreds(),
519 layout_margin=10, layout_spacing=15, modal=True)
520
521 def setupEditor(self, parent_layout):
522 self.edition_layout = CredEditor(self)
523 parent_layout.addLayout(self.edition_layout)
524
525 def setListItems(self):
526 self.listview.clear()
527 self.rootitem = CredRootItem(self.listview, self.title, self.model_object)
528 for obj in self.model_object.getCreds():
529 self.rootitem.addCred(obj)
530
531 def setEdition(self):
532 if self._current_item is not None:
533 if self._current_item.type == "Cred":
534 self.edition_layout.setCred(self._current_item.getModelObject())
535
536 def saveValue(self):
537 if self._current_item is not None:
538 if self._current_item.type == "Cred":
539 cred = self._current_item.getModelObject()
540 kwargs = self.edition_layout.getArgs()
541 if kwargs["username"] and kwargs["password"]:
542 guiapi.editCred(cred, **kwargs)
543 self.setListItems()
544
545 def addValue(self):
546 dialog = NewCredDialog(self, callback=self.__addValue)
547 dialog.exec_loop()
548
549 def __addValue(self, *args):
550 obj = self.rootitem.getModelObject()
551 guiapi.createAndAddCred(obj, *args)
552 self.setListItems()
553
554 def removeValue(self):
555 for item in self._selected_items:
556 if item.type == "Cred":
557 cred = item.getModelObject()
558 guiapi.delCred(cred.getParent().getID(), cred.getID())
559 self.setListItems()
560 self.edition_layout.clear()
561
562
563 def sizeHint(self):
564 return qt.QSize(750, 500)
565
566
567 class AboutDialog(BaseDialog):
568
569 def __init__(self, parent):
570 BaseDialog.__init__(self, parent, "AboutDialog",
571 layout_margin=15, layout_spacing=10, modal=True)
572 self.setCaption( 'About %s' % CONF.getAppname() )
573
574 self.logo = qt.QPixmap( os.path.join(CONF.getImagePath(),"about.png") )
575 self.logolabel = qt.QLabel( self )
576 self.logolabel.setPixmap( self.logo )
577 self.logolabel.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
578
579 self._about_text = u"""%s v%s""" % (CONF.getAppname(),CONF.getVersion())
580 self._about_text += "\nInfobyte LLC. All rights reserved"
581
582 self.text = qt.QLabel( self._about_text, self )
583 self.text.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
584
585 self.layout.addWidget( self.logolabel )
586 self.layout.addWidget( self.text )
587 self.setupButtons({"ok" : None})
588
589
590
591 class RepositoryConfigDialog(BaseDialog):
592
593 def __init__(self, parent, url="http://example:5984", replication = False, replics = "", callback=None):
594 BaseDialog.__init__(self, parent, "RepositoryConfig",
595 layout_margin=25, layout_spacing=20, modal=True)
596
597 self._callback = callback
598
599 self.setCaption("Repository Configuration")
600
601 hbox1 = qt.QHBox(self)
602 hbox1.setSpacing(10)
603 self._repourl_label = qt.QLabel("CouchDB (http://127.0.0.1:5984)", hbox1)
604 self._repourl_edit = qt.QLineEdit(hbox1)
605 if url: self._repourl_edit.setText(url)
606 self.layout.addWidget(hbox1)
607
608 hbox2 = qt.QHBox(self)
609 hbox2.setSpacing(5)
610 self._replicate_label = qt.QLabel("Replication enabled", hbox2)
611 self._replicate_edit = qt.QCheckBox(hbox2)
612 self._replicate_edit.setChecked(replication)
613
614 self.layout.addWidget(hbox2)
615
616 hbox3 = qt.QHBox(self)
617 hbox3.setSpacing(10)
618 self._replics_label = qt.QLabel("Replics", hbox3)
619 self.__replics_edit = qt.QLineEdit(hbox3)
620 if replics: self.__replics_edit.setText(replics)
621 self.layout.addWidget(hbox3)
622
623 self.__repourl_txt = self._repourl_edit.text()
624 self.__is_replicated_bool = self._replicate_edit.isChecked()
625 self.__replics_list_txt = self.__replics_edit.text()
626
627
628 self.setupButtons({ "ok" : self.ok_pressed,
629 "cancel" : None
630 })
631
632 def getData(self):
633 self.__repourl_txt = self._repourl_edit.text()
634 self.__is_replicated_bool = self._replicate_edit.isChecked()
635 self.__replics_list_txt = self.__replics_edit.text()
636 return (self.__repourl_txt.latin1(),
637 self.__is_replicated_bool,
638 self.__replics_list_txt.latin1())
639
640 def ok_pressed(self):
641 if self._callback is not None:
642 self._callback(*self.getData())
643 self.accept()
644
645
646
647 class ExceptionDialog(BaseDialog):
648
649 def __init__(self, parent, text="", callback=None, excection_objects=None):
650 BaseDialog.__init__(self, parent, "ExceptionDialog",
651 layout_margin=10, layout_spacing=15, modal=True)
652 self._callback = callback
653 self._excection_objects = excection_objects
654 self.setCaption('Error')
655
656 label1 = qt.QLabel("An unhandled error ocurred...", self )
657 label1.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
658 self.layout.addWidget(label1)
659
660 exception_textedit = qt.QTextEdit(self)
661 exception_textedit.setTextFormat(qt.Qt.LogText)
662 exception_textedit.append(text)
663 self.layout.addWidget(exception_textedit)
664
665 label2 = qt.QLabel("""Do you want to collect information and send it to Faraday developers?\n\
666 If you press Cancel the application will just continue.""", self )
667 label2.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
668 self.layout.addWidget(label2)
669
670 self.setupButtons({ "ok" : self.ok_pressed,
671 "cancel" : None
672 })
673 def ok_pressed(self):
674 if self._callback is not None:
675 self._callback(*self._excection_objects)
676 self.accept()
677
678 def sizeHint(self):
679 return qt.QSize(680, 300)
680
681
682 class SimpleDialog(BaseDialog):
683
684 def __init__(self, parent, text="", type="Information"):
685 BaseDialog.__init__(self, parent, "SimpleDialog",
686 layout_margin=10, layout_spacing=10, modal=True)
687 self.setCaption(type)
688
689
690 self.text = qt.QLabel(self)
691 self.text.setTextFormat(qt.Qt.RichText)
692 self.text.setText(text.replace("\n", "<br>"))
693 self.text.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
694 self.layout.addWidget( self.text )
695 self.setupButtons({"ok" : None})
696
697
698 class ExitDialog(BaseDialog):
699 def __init__(self, parent, callback=None,title="Exit", msg="Are you sure?"):
700 BaseDialog.__init__(self, parent, "ExitDialog",
701 layout_margin=20, layout_spacing=15, modal=True)
702 self.setCaption(title)
703
704 hbox1 = qt.QHBox(self)
705 hbox1.setSpacing(5)
706 self._message_label = qt.QLabel(msg, hbox1)
707 self._message_label.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
708 self.layout.addWidget(hbox1)
709 self.setupButtons({ "ok" : callback,
710 "cancel" : None
711 })
712
713 def sizeHint(self):
714 return qt.QSize(50, 50)
715
716
717
718 class MessageDialog(BaseDialog):
719 def __init__(self, parent, callback=None , title="Are you sure?", msg="Are you sure?", item=None):
720 BaseDialog.__init__(self, parent, "ExitDialog",
721 layout_margin=20, layout_spacing=15, modal=True)
722 self.setCaption(title)
723
724 self._callback = callback
725 self._item=item
726 hbox1 = qt.QHBox(self)
727 hbox1.setSpacing(5)
728 self._message_label = qt.QLabel(msg, hbox1)
729 self._message_label.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
730 self.layout.addWidget(hbox1)
731 self.setupButtons({ "ok" : self.ok_pressed,
732 "cancel" : None
733 })
734 def ok_pressed(self):
735 if self._callback is not None:
736 self._callback(self._item)
737 self.accept()
738
739 def sizeHint(self):
740 return qt.QSize(50, 50)
741
742
743
744 class VulnDialog(BaseDialog):
745
746 def __init__(self, parent, name="",description="", ref="", callback=None, item=None):
747 BaseDialog.__init__(self, parent, "VulnDialog",
748 layout_margin=10, layout_spacing=15, modal=True)
749
750 self._item = item
751 self._callback = callback
752 self.setCaption("New vulnerability" if name is "" else "Vuln %s" % item.name)
753
754
755
756 hbox1 = qt.QHBox(self)
757 hbox1.setSpacing(5)
758 name_label = qt.QLabel("Name", hbox1)
759 self._name_edit = qt.QLineEdit(hbox1)
760 if name: self._name_edit.setText(name)
761 self.layout.addWidget(hbox1)
762
763 hbox2 = qt.QHBox(self)
764 hbox2.setSpacing(5)
765 ref_label = qt.QLabel("Ref", hbox2)
766 self._ref_edit = qt.QLineEdit(hbox2)
767 if ref: self._ref_edit.setText(ref)
768 self.layout.addWidget(hbox2)
769
770 vbox6 = qt.QVBox(self)
771 vbox6.setSpacing(5)
772 description_label = qt.QLabel("Description:", vbox6 )
773
774 self._description_edit = qt.QTextEdit(vbox6)
775 self._description_edit.setTextFormat(qt.Qt.PlainText)
776 if description: self._description_edit.append(description)
777 self.layout.addWidget(vbox6)
778
779 self.setupButtons({ "ok" : self.ok_pressed,
780 "cancel" : None
781 })
782 def ok_pressed(self):
783 if self._callback is not None:
784 if self._name_edit.text() != "":
785 self._callback("%s" % self._name_edit.text(),"%s" % self._description_edit.text(),
786 "%s" % self._ref_edit.text(),self._item)
787 self.accept()
788 else:
789 dialog = SimpleDialog(self, "Please select a name")
790 dialog.exec_loop()
791
792 def sizeHint(self):
793 return qt.QSize(600, 400)
794
795
796 class CategoryDialog(BaseDialog):
797
798 def __init__(self, parent, name="", callback=None, item=None):
799 BaseDialog.__init__(self, parent, "CategoryDialog",
800 layout_margin=10, layout_spacing=15, modal=True)
801
802 self._item = item
803 self._callback = callback
804 self.setCaption("New category" if name is "" else "Category in %s" % item.name)
805
806 hbox1 = qt.QHBox(self)
807 hbox1.setSpacing(5)
808 name_label = qt.QLabel("Name", hbox1)
809 self._name_edit = qt.QLineEdit(hbox1)
810 if name: self._name_edit.setText(name)
811 self.layout.addWidget(hbox1)
812
813 self.setupButtons({ "ok" : self.ok_pressed,
814 "cancel" : None
815 })
816
817 def ok_pressed(self):
818 if self._callback is not None:
819 if self._name_edit.text() != "":
820 self._callback("%s" % self._name_edit.text(), self._item)
821 self.accept()
822 else:
823 dialog = SimpleDialog(self, "Please select a name")
824 dialog.exec_loop()
825
826 def sizeHint(self):
827 return qt.QSize(600, 400)
828
829
830
831 class NoteDialog(BaseDialog):
832
833 def __init__(self, parent, name="", text="", callback=None, item=None):
834 BaseDialog.__init__(self, parent, "NoteDialog",
835 layout_margin=10, layout_spacing=15, modal=True)
836
837 self._item = item
838 self._callback = callback
839 self.setCaption("New note" if name is "" else "Note %d" % item.id)
840
841 hbox1 = qt.QHBox(self)
842 hbox1.setSpacing(5)
843 name_label = qt.QLabel("Name", hbox1)
844 self._name_edit = qt.QLineEdit(hbox1)
845 if name: self._name_edit.setText(name)
846 self.layout.addWidget(hbox1)
847
848 vbox2 = qt.QVBox(self)
849 vbox2.setSpacing(3)
850 content_label = qt.QLabel("Note content:", vbox2 )
851
852 self._textedit = qt.QTextEdit(vbox2)
853 self._textedit.setTextFormat(qt.Qt.PlainText)
854 if text: self._textedit.append(text)
855 self.layout.addWidget(vbox2)
856
857 self.setupButtons({ "ok" : self.ok_pressed,
858 "cancel" : None
859 })
860 def ok_pressed(self):
861 if self._callback is not None:
862 if self._name_edit.text() != "":
863 if self._item is not None:
864 self._callback(self._name_edit.text(), self._textedit.text(),self._item)
865 else:
866 self._callback(self._name_edit.text(), self._textedit.text())
867 self.accept()
868 else:
869 dialog = SimpleDialog(self, "Please select a name")
870 dialog.exec_loop()
871
872 def sizeHint(self):
873 return qt.QSize(600, 400)
874
875
876
877
878 class NotificationWidget(qt.QLabel):
879 def __init__(self, parent, text=""):
880 qt.QLabel.__init__(self, parent, "notification")
881 pal = qt.QPalette()
882 color = qt.QColor(232, 226, 179, qt.QColor.Rgb)
883 pal.setColor(qt.QColorGroup.Background, color)
884 self.setTextFormat(qt.Qt.RichText)
885 self.setText(text.replace("\n", "<br>"))
886 self.setFrameStyle(qt.QFrame.PopupPanel | qt.QFrame.Plain)
887 self.setAlignment( qt.Qt.AlignHCenter | qt.Qt.AlignVCenter )
888 self.setPalette(pal)
889
890 _w,_h=self._getsize(text)
891 self.resize(qt.QSize(_w,_h))
892
893 self._updatePos(parent)
894
895 def _getsize(self, text):
896 _tlist=text.split("\n")
897 _width=0
898 _w=10
899 for i in _tlist:
900 _size=len(i)
901 if _size > _width:
902 _width = _size
903 if _size > 80 and len(i.split(" ")) <=2:
904 _w=12
905
906
907 return _width*_w,(28*len(text.split("\n")))
908
909
910 def _updatePos(self, parent):
911 pos = qt.QPoint()
912 pos.setX(parent.width() - self.width() - 5)
913 pos.setY(parent.height() - self.height() - 20)
914 self.move(pos)
915
916 def closeNotification(self):
917 self.hide()
918 parent = self.parent()
919 parent.removeChild(self)
920 self.destroy()
921
922
923
924 class WorkspacePropertiesDialog(BaseDialog):
925
926 def __init__(self, parent, text="", callback=None, workspace=None):
927 BaseDialog.__init__(self, parent, "WorkspacePropertiesDialog",
928 layout_margin=10, layout_spacing=15, modal=True)
929 self._callback = callback
930 self.setCaption('Workspace Properties')
931
932
933 hbox1 = qt.QHBox(self)
934 hbox1.setSpacing(5)
935 self._name_label = qt.QLabel("Name", hbox1)
936 self._name_edit = qt.QLineEdit(hbox1)
937 self.layout.addWidget(hbox1)
938
939 hbox2 = qt.QHBox(self)
940 self._sdate_edit = qt.QDateEdit(hbox2, "start_date")
941 self.layout.addWidget(hbox2)
942
943 hbox3 = qt.QHBox(self)
944 self._fdate_edit = qt.QDateEdit(hbox3, "ftart_date")
945 self.layout.addWidget(hbox3)
946
947 hbox4 = qt.QHBox(self)
948 self._shared_checkbox = qt.QCheckBox("Shared", hbox4, "shared")
949 self.layout.addWidget(hbox4)
950
951 hbox5 = qt.QHBox(self)
952 hbox5.setSpacing(10)
953 self._desc_label = qt.QLabel("Description", hbox5)
954 self._desc_edit = qt.QTextEdit(hbox5)
955 self.layout.addWidget(hbox5)
956
957
958
959 self.setupButtons({ "ok" : self.ok_pressed,
960 "cancel" : None
961 })
962
963 def ok_pressed(self):
964
965
966 if self._callback is not None:
967 name = self._name_edit.text()
968 description = self._desc_edit.text()
969 sdate = self._sdate_edit.date.toString()
970 fdate = self._fdate_edit.date.toString()
971 shared = self._shared_checkbox.checked
972 self._callback()
973 self.accept()
974
975 def sizeHint(self):
976 return qt.QSize(600, 400)
977
978
979
980
981 class WorkspaceCreationDialog(BaseDialog):
982
983 def __init__(self, parent, text="", callback=None, workspace=None, workspace_manager=None):
984 BaseDialog.__init__(self, parent, "WorkspaceCreationDialog",
985 layout_margin=10, layout_spacing=15, modal=True)
986 self._callback = callback
987 self.setCaption('New Workspace')
988 self._main_window = parent
989
990 hbox1 = qt.QHBox(self)
991 hbox1.setSpacing(5)
992 self._name_label = qt.QLabel("Name", hbox1)
993 self._name_edit = qt.QLineEdit(hbox1)
994 self.layout.addWidget(hbox1)
995
996 hbox2 = qt.QHBox(self)
997 hbox2.setSpacing(10)
998 self._desc_label = qt.QLabel("Description", hbox2)
999 self._desc_edit = qt.QTextEdit(hbox2)
1000 self.layout.addWidget(hbox2)
1001
1002 hbox3 = qt.QHBox(self)
1003 hbox3.setSpacing(10)
1004 self._type_label = qt.QLabel("Type", hbox3)
1005 self._type_combobox = qt.QComboBox(hbox3)
1006 self._type_combobox.setEditable(False)
1007 for w in workspace_manager.getAvailableWorkspaceTypes():
1008 self._type_combobox.insertItem(w)
1009 self.layout.addWidget(hbox3)
1010
1011 if len(workspace_manager.getAvailableWorkspaceTypes()) <= 1:
1012 parent.showPopup("No Couch Configuration available. Config, more workpsaces flavors")
1013
1014 self.__name_txt = self._name_edit.text()
1015 self.__desc_txt = self._desc_edit.text()
1016 self.__type_txt = str(self._type_combobox.currentText())
1017
1018 self.setupButtons({ "ok" : self.ok_pressed,
1019 "cancel" : None
1020 })
1021 def ok_pressed(self):
1022 res = re.match(r"^[a-z][a-z0-9\_\$()\+\-\/]*$", str(self._name_edit.text()))
1023 if res:
1024 if self._callback is not None:
1025 self.__name_txt = str(self._name_edit.text())
1026 self.__desc_txt = str(self._desc_edit.text())
1027 self.__type_txt = str(self._type_combobox.currentText())
1028 self._callback(self.__name_txt, self.__desc_txt, self.__type_txt)
1029 self.accept()
1030 else:
1031 self._main_window.showPopup("A workspace must be named with all lowercase letters (a-z), digits (0-9) or any of the _$()+-/ characters. The name has to start with a lowercase letter (a-z)")
1032
1033
1034
1035 class PluginSettingsDialog(BaseDialog, PluginSettingsUi):
1036 def __init__(self, parent=None, plugin_manager=None):
1037 BaseDialog.__init__(self, parent, "")
1038 PluginSettingsUi.__init__(self, parent)
1039
1040 self._plugin_manager = plugin_manager
1041 if plugin_manager is not None:
1042 self._plugin_settings = plugin_manager.getSettings()
1043 else:
1044 self._plugin_settings = {}
1045
1046 self._set_connections()
1047
1048 self._items = {}
1049 self._params = {}
1050
1051 self.t_parameters.horizontalHeader().setStretchEnabled(True, 0)
1052
1053 self._selected_plugin = None
1054 self._load_plugin_list()
1055
1056 def _set_connections(self):
1057 self.connect(self.lw_plugins, qt.SIGNAL("selectionChanged(QListViewItem*)"), self._show_plugin )
1058 self.connect(self.lw_plugins, qt.SIGNAL("clicked(QListViewItem*)"),
1059 self._show_plugin)
1060 self.connect(self.t_parameters, qt.SIGNAL("valueChanged(int, int)"),
1061 self._set_parameter)
1062 self.connect(self.bt_ok, qt.SIGNAL("clicked()"),
1063 self._update_settings)
1064
1065 def _load_plugin_list(self):
1066 if self._plugin_manager is None:
1067 return
1068
1069 for plugin_id, params in self._plugin_settings.iteritems():
1070 new_item = qt.QListViewItem(self.lw_plugins, "%s" % params["name"])
1071 self._items[new_item] = plugin_id
1072
1073 def _set_parameter(self, row, col):
1074 settings = self._plugin_settings[self._selected_plugin]["settings"]
1075 parameter = self.t_parameters.verticalHeader().label(row)
1076 value = self.t_parameters.text(row, col)
1077 settings[str(parameter).strip()] = str(value).strip()
1078
1079 def _update_settings(self):
1080 if self._plugin_manager is not None:
1081 self._plugin_manager.updateSettings(self._plugin_settings)
1082
1083 def _show_plugin(self, item):
1084 if item is None:
1085 return
1086
1087 self.t_parameters.removeRows(range(self.t_parameters.numRows()))
1088
1089 plugin_id = self._items[item]
1090 self._selected_plugin = plugin_id
1091
1092 params = self._plugin_settings[plugin_id]
1093
1094 self.le_name.setText(params["name"])
1095 self.le_version.setText(params["version"])
1096 self.le_pversion.setText(params["plugin_version"])
1097
1098 for setting, value in params["settings"].iteritems():
1099 index = self.t_parameters.numRows()
1100 self.t_parameters.insertRows(index)
1101
1102 self.t_parameters.verticalHeader().setLabel(index, setting)
1103 self.t_parameters.setText(index, 0, str(value))
1104
1105
1106
1107 class VulnsListDialog(BaseDialog, VulnerabilitiesUi):
1108 def __init__(self, parent=None,item=None):
1109 BaseDialog.__init__(self, parent, "")
1110 VulnerabilitiesUi.__init__(self, parent)
1111 self._vulns = []
1112 self._setup_signals()
1113 self._item=item
1114
1115 self.t_vulns.setColumnReadOnly(0, True)
1116 self.t_vulns.setColumnReadOnly(1, True)
1117 self.t_vulns.setColumnReadOnly(2, True)
1118
1119 self.t_vulns.horizontalHeader().setStretchEnabled(True, 3)
1120
1121 def add_vuln(self, vuln):
1122 index = self.t_vulns.numRows()
1123 self._vulns.append(vuln)
1124 self.t_vulns.insertRows(index)
1125
1126 self.t_vulns.setText(index, 0, str(vuln.name))
1127 self.t_vulns.setText(index, 1, str(vuln.refs))
1128 self.t_vulns.setText(index, 2, str(vuln.desc))
1129
1130 self.t_vulns.adjustColumn(0)
1131 self.t_vulns.adjustColumn(1)
1132 self.t_vulns.adjustColumn(2)
1133
1134
1135 def del_vuln(self, vuln):
1136
1137 index = self.t_vulns.currentRow()
1138 self._vulns.remove(vuln)
1139 self.t_vulns.removeRows([index])
1140
1141
1142
1143 def _setup_signals(self):
1144
1145 self.connect(self.t_vulns, SIGNAL("doubleClicked(int,int,int,QPoint)"),self._edit)
1146
1147 self.connect(self.add_button, SIGNAL("clicked()"), self._add)
1148 self.connect(self.edit_button, SIGNAL("clicked()"), self._edit)
1149 self.connect(self.delete_button, SIGNAL("clicked()"), self._delete)
1150 self.connect(self.list_note_button, SIGNAL("clicked()"), self._list_note)
1151 self.connect(self.manage_evidence_button, SIGNAL("clicked()"), self._evidence)
1152
1153
1154 def _edit(self):
1155
1156 if self.t_vulns.currentSelection() != -1:
1157 _object=self._vulns[self.t_vulns.currentRow()]
1158 dialog = VulnDialog(self,str(_object.name),str(_object.desc),str(_object.refs),self._editcallback,_object)
1159 res = dialog.exec_loop()
1160
1161 def _evidence(self):
1162
1163 if self.t_vulns.currentSelection() != -1:
1164 _object=self._vulns[self.t_vulns.currentRow()]
1165 _object.object = _object
1166 dialog = EvidencesListDialog(self, _object)
1167
1168
1169 dialog.exec_loop()
1170
1171 def _list_note(self):
1172
1173 if self.t_vulns.currentSelection() != -1:
1174 _object=self._vulns[self.t_vulns.currentRow()]
1175 _object.object = _object
1176 dialog = NotesListDialog(self, _object)
1177
1178
1179 dialog.exec_loop()
1180
1181 def _editcallback(self,name,desc,ref,item):
1182
1183 item.name=name
1184 item.desc=desc
1185 item.ref=ref
1186
1187 self.t_vulns.setText(self.t_vulns.currentRow(), 0, name)
1188 self.t_vulns.setText(self.t_vulns.currentRow(), 1, ref)
1189 self.t_vulns.setText(self.t_vulns.currentRow(), 2, desc)
1190
1191 def _newcallback(self, name, desc, ref, item):
1192 _parent=self._item.object.getParent()
1193
1194 api.devlog("newVuln (%s) (%s) (%s) (%s) " % (name, desc, ref, item.object.getName(),))
1195
1196 _newvuln=api.newVuln(name,desc,ref)
1197
1198 if item.type == "Application":
1199 api.addVulnToApplication(_newvuln,_parent.name,item.object.getName())
1200 elif item.type == "Interface":
1201 api.addVulnToInterface(_newvuln,_parent.name,item.object.getName())
1202 elif item.type == "Host":
1203 api.addVulnToHost(_newvuln,item.object.getName())
1204 elif item.type == "Service":
1205 api.addVulnToService(_newvuln,_parent.name,item.object.getName())
1206
1207
1208 self.add_vuln(_newvuln)
1209
1210 def _add(self):
1211 if self._item is not None and self._item.object is not None:
1212 dialog = VulnDialog(self,callback=self._newcallback,item=self._item)
1213 res = dialog.exec_loop()
1214
1215
1216 def _delete(self):
1217 if self.t_vulns.currentSelection() != -1:
1218 _vuln=self._vulns[self.t_vulns.currentRow()]
1219 _parent=_vuln._parent
1220
1221
1222 if isinstance(_parent,hosts.HostApplication):
1223 api.delVulnFromApplication(_vuln.getID(),_parent.getParent().name,_parent.name)
1224 elif isinstance(_parent,hosts.Interface):
1225 api.delVulnFromInterface(_vuln.getID(),_parent.getParent().name,_parent.name)
1226 elif isinstance(_parent,hosts.Host):
1227 api.delVulnFromHost(_vuln.getID(),_parent.name)
1228 elif isinstance(_parent,hosts.Service):
1229 api.delVulnFromService(_vuln.getID(),_parent.getParent().name,_parent.name)
1230
1231
1232 self.del_vuln(_vuln)
1233
1234
1235
1236
1237 class PreferencesDialog(BaseDialog, PreferencesUi):
1238 def __init__(self, parent=None):
1239 BaseDialog.__init__(self, parent, "")
1240 PreferencesUi.__init__(self, parent)
1241 self._main_window = parent
1242
1243 self._fdb = qt.QFontDatabase()
1244 self._families = self._fdb.families()
1245 self.cb_font_family.insertStringList(self._families)
1246
1247 self._styles = None
1248 self._sizes = None
1249
1250 self._family = None
1251 self._style = None
1252 self._size = None
1253
1254
1255 self._set_connections()
1256 self._load_styles(0)
1257 self._load_sizes(0)
1258
1259 def _set_connections(self):
1260 self.connect(self.cb_font_family, SIGNAL("activated(int)"),
1261 self._load_styles)
1262 self.connect(self.cb_font_style, SIGNAL("activated(int)"),
1263 self._load_sizes)
1264 self.connect(self.cb_font_size, SIGNAL("activated(int)"),
1265 self._change_size)
1266 self.connect(self.bt_ok, SIGNAL("clicked()"),
1267 self.accept)
1268 self.connect(self.bt_cancel, SIGNAL("clicked()"),
1269 self.reject)
1270
1271 def _load_styles(self, index):
1272 self._family = self._families[index]
1273 self.cb_font_style.clear()
1274 self._styles = self._fdb.styles(self._family)
1275 self.cb_font_style.insertStringList(self._styles)
1276 self._update_font()
1277
1278 def _load_sizes(self, index):
1279 self._style = self._styles[index]
1280 self.cb_font_size.clear()
1281 self._sizes = self._fdb.smoothSizes(self._family, self._style)
1282 string_list = QStringList()
1283 [string_list.append(str(size)) for size in self._sizes]
1284 self.cb_font_size.insertStringList(string_list)
1285 self._update_font()
1286
1287 def _change_size(self, index):
1288 self._size = self._sizes[index]
1289 self._update_font()
1290
1291 def _update_font(self):
1292 font = self.le_example.font()
1293 if self._family is not None:
1294 font.setFamily(self._family)
1295 if self._size is not None:
1296 font.setPointSize(self._size)
1297 if self._style is not None:
1298 isItalic = self._fdb.italic(self._family, self._style)
1299 font.setItalic(isItalic)
1300 isBold = self._fdb.bold(self._family, self._style)
1301 font.setBold(isBold)
1302 weight = self._fdb.weight(self._family, self._style)
1303 font.setWeight(weight)
1304 self.le_example.setFont(font)
1305 self._main_window.shell_font = font
1306
1307
1308
1309 class NotesListDialog(BaseDialog, NotesListUI):
1310
1311 def __init__(self, parent, item=None):
1312 BaseDialog.__init__(self, parent, "NotesListDialog", modal=True)
1313 NotesListUI.__init__(self, parent)
1314 self.notes_table.setColumnReadOnly(0, True)
1315 self.notes_table.setColumnReadOnly(1, True)
1316 self._notes = []
1317 self._setup_signals()
1318 self._item = item
1319 if item is not None and item.object is not None:
1320 for n in item.object.getNotes():
1321 self.add_note_to_table(n)
1322
1323 def add_note_to_table(self, note):
1324 index = self.notes_table.numRows()
1325 self._notes.append(note)
1326 self.notes_table.insertRows(index)
1327 self.notes_table.setText(index, 0, note.name)
1328 self.notes_table.setText(index, 1, note.text)
1329 self.notes_table.adjustColumn(0)
1330 self.notes_table.adjustColumn(1)
1331 self.notes_table.adjustRow(index)
1332
1333 def _setup_signals(self):
1334
1335
1336
1337
1338 self.connect(self.add_button, SIGNAL("clicked()"), self._add_note)
1339 self.connect(self.edit_button, SIGNAL("clicked()"), self._edit_note)
1340 self.connect(self.delete_button, SIGNAL("clicked()"), self._delete_note)
1341 self.connect(self.list_note_button, SIGNAL("clicked()"), self._list_note)
1342
1343 def _edit_note(self):
1344 if self.notes_table.currentSelection() != -1:
1345 _object=self._notes[self.notes_table.currentRow()]
1346 dialog = NoteDialog(self,_object.name,_object.text,self._editcallbackNote,_object)
1347 res = dialog.exec_loop()
1348
1349 def _list_note(self):
1350
1351 if self.notes_table.currentSelection() != -1:
1352 _object=self._notes[self.notes_table.currentRow()]
1353 _object.object = _object
1354 dialog = NotesListDialog(self, _object)
1355
1356
1357 dialog.exec_loop()
1358
1359 def _editcallbackNote(self,name,text,item):
1360 item.name = name
1361 item.text = text
1362 self.notes_table.setText(self.notes_table.currentRow(), 0, name)
1363 self.notes_table.setText(self.notes_table.currentRow(), 1, text)
1364
1365 def _newcallbackNote(self, name, text, item):
1366 _parent=self._item.object.getParent()
1367
1368 api.devlog("newNote (%s) (%s) (%s) " % (name, text, item.object.getName(),))
1369
1370 _newnote=api.newNote(name,text)
1371
1372 if item.object.class_signature == "HostApplication":
1373 api.addNoteToApplication(_newnote,_parent.name,item.object.getName())
1374 elif item.object.class_signature == "Interface":
1375 api.addNoteToInterface(_newnote,_parent.name,item.object.getName())
1376 elif item.object.class_signature == "Host":
1377 api.addNoteToHost(_newnote,item.object.getName())
1378 elif item.object.class_signature == "Service":
1379 api.addNoteToService(_newnote,_parent.name,item.object.getName())
1380 else:
1381
1382
1383 item.object.addNote(_newnote)
1384
1385 self.add_note_to_table(_newnote)
1386
1387 def _add_note(self):
1388 if self._item is not None and self._item.object is not None:
1389 dialog = NoteDialog(self,callback=self._newcallbackNote,item=self._item)
1390 res = dialog.exec_loop()
1391
1392
1393 def _delete_note(self):
1394
1395 _object=self._notes[self.notes_table.currentRow()]
1396 if self.notes_table.currentSelection() != -1:
1397 _note=self._notes[self.notes_table.currentRow()]
1398 _parent=_note._parent
1399
1400 if _parent.class_signature == "HostApplication":
1401 api.delNoteFromApplication(_note.getID(),_parent.getParent().name,_parent.name)
1402 elif _parent.class_signature == "Interface":
1403 api.delNoteFromInterface(_note.getID(),_parent.getParent().name,_parent.name)
1404 elif _parent.class_signature == "Host":
1405 api.delNoteFromHost(_note.getID(),_parent.name)
1406 elif _parent.class_signature == "Service":
1407 api.delNoteFromService(_note.getID(),_parent.getParent().name,_parent.name)
1408 else:
1409 _parent.delNote(_note.getID())
1410
1411
1412
1413 self.del_note(_note)
1414
1415
1416 def del_note(self, note):
1417
1418 index = self.notes_table.currentRow()
1419 self._notes.remove(note)
1420 self.notes_table.removeRows([index])
1421
1422
1423
1424
1425
1426 class EvidencesListDialog(BaseDialog, EvidencesListUI):
1427
1428 def __init__(self, parent, item=None):
1429 BaseDialog.__init__(self, parent, "EvidencesListDialog", modal=True)
1430 EvidencesListUI.__init__(self, parent)
1431 self.evidences_table.setColumnReadOnly(0, True)
1432 self.evidences_table.setColumnReadOnly(1, True)
1433 self._setup_signals()
1434 self._item = item
1435 if item is not None and item.object is not None:
1436 for n in item.object.evidences:
1437 self.add_evidence_to_table(n)
1438
1439 def add_evidence_to_table(self, evidence):
1440 index = self.evidences_table.numRows()
1441 self.evidences_table.insertRows(index)
1442 self.evidences_table.setText(index, 0, str(index))
1443 self.evidences_table.setText(index, 1, evidence)
1444 self.evidences_table.adjustColumn(0)
1445 self.evidences_table.adjustColumn(1)
1446 self.evidences_table.adjustRow(index)
1447
1448 def _setup_signals(self):
1449
1450
1451
1452
1453 self.connect(self.add_button, SIGNAL("clicked()"), self._add_evidence)
1454 self.connect(self.delete_button, SIGNAL("clicked()"), self._delete_evidence)
1455
1456 def _newcallbackEvidence(self, name, item):
1457
1458 d_path = api.addEvidence("%s" % name)
1459 if d_path is not False:
1460 self._item.object.evidences.append(d_path)
1461 self.add_evidence_to_table(d_path)
1462
1463 def _add_evidence(self):
1464 if self._item is not None and self._item.object is not None:
1465 filename = QFileDialog.getOpenFileName(
1466 CONF.getDefaultTempPath(),
1467 "Images Files (*.png)",
1468 None,
1469 "open file dialog",
1470 "Choose a file to add in the evidence" );
1471
1472 if (filename):
1473 self._newcallbackEvidence(filename,self._item)
1474 for n in self._item.object.evidences:
1475 api.devlog("Los items screenshot son:" + n)
1476
1477
1478 def _delete_evidence(self):
1479
1480 if self.evidences_table.currentSelection() != -1:
1481
1482 index = self.evidences_table.currentRow()
1483 _evidence=self._item.object.evidences[index]
1484 self._item.object.evidences.remove(_evidence)
1485 api.delEvidence(_evidence)
1486 self.evidences_table.removeRows([index])
1487 self._updateIds()
1488 for n in self._item.object.evidences:
1489 api.devlog("Los items screenshot son:" + n)
1490
1491 def _updateIds(self):
1492 for i in range(0,self.evidences_table.numRows()):
1493 self.evidences_table.setText(i , 0, str(i))
1494
1495
1496
1497
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 import qt
7 from threading import Lock
8 from gui.qt3.modelobjectitems import *
9 import model.api as api
10 import model.guiapi as guiapi
11 import re as re
12 from gui.qt3.dialogs import NewVulnDialog, ConflictResolutionDialog, MessageDialog, NotesDialog, VulnsDialog, CredsDialog
13 from gui.qt3.customevents import *
14 from gui.qt3.dialogs import WorkspacePropertiesDialog
15 from gui.qt3.edition import EditionTable, NewServiceDialog, NewHostDialog, NewInterfaceDialog, NewCredDialog, NewNoteDialog
16
17 from config.configuration import getInstanceConfiguration
18 CONF = getInstanceConfiguration()
19
20 from whoosh.index import create_in
21 from whoosh.fields import *
22
23
24 class PropertyItem(qt.QListViewItem):
25
26
27 """Item for displaying a preferences-set in HostsBrowser."""
28 def __init__(self, settings, number, parent):
29 """_plugin_settings is the _plugin_settings class to work for
30 parent is the parent ListViewItem (of type ModelObjectListViewItem)
31 """
32 qt.QListViewItem.__init__(self, parent)
33 self.settings = settings
34 self.parent = parent
35 self.widget = None
36 self.setText(0, settings.name)
37 self.setText(1, 'setting')
38 self.index = number
39
40 def compare(self, i, col, ascending):
41 """Always sort according to the index value."""
42
43 a = [-1, 1][ascending]
44
45 if self.index < i.index:
46 return -1*a
47 elif self.index > i.index:
48 return 1*a
49 else:
50 return 0
51
52
53 class ModelObjectListView(qt.QListView):
54 """
55 List view for hosts
56 It allows Drag and Drop (TODO)
57 """
58 def __init__(self, parent=None):
59 qt.QListView.__init__(self, parent)
60 self.setSelectionMode(qt.QListView.Extended)
61
62
63
64
65
66
67
68 def selectWidget(self, widget):
69 """Find the widget in the list and select it."""
70
71
72
73 iter = qt.QListViewItemIterator(self)
74
75 found = None
76 while True:
77 item = iter.current()
78 if item == None:
79 break
80 if item.widget == widget:
81 found = item
82 break
83 iter += 1
84
85 if found:
86 self.ensureItemVisible(found)
87 self.setSelected(found, True)
88
89 def sizeHint(self):
90 return qt.QSize(500, 800)
91
92
93 class SaveButton(qt.QPushButton):
94 def __init__(self, parent, callback):
95 qt.QPushButton.__init__(self, "Save", parent)
96 parent.connect(self, qt.SIGNAL('clicked()'), callback)
97 self.setMaximumSize(qt.QSize(75, 25))
98
99
100 class HostsBrowser(qt.QVBox):
101 """Tree view to display Hosts"""
102
103 def __init__(self, parent, model_controller, caption):
104 qt.QVBox.__init__(self, parent)
105
106 self._model_controller = model_controller
107
108 self.modelUpdateTimer = qt.QTimer(self)
109
110 self.__pendingModelObjectRedraws = []
111
112 self.reindex_flag_lock = Lock()
113 self.reindex_flag = False
114
115 self.connect( self.modelUpdateTimer, qt.SIGNAL("timeout()"), self._modelObjectViewUpdater)
116
117 self.modelUpdateTimer.start( 1000 , False)
118
119
120 self.setName(caption) if caption else self.setName("")
121
122 self.setFrameStyle(qt.QFrame.Panel | qt.QFrame.Plain)
123 self.setLineWidth(0)
124
125 self._host_items = {}
126
127
128 self._category_items = {}
129
130
131
132 self._category_tree = {}
133
134 self.contextpopups = {}
135 self._setupContextPopups()
136
137 self.contextdispatchers = {}
138
139
140 self._filter = ""
141 self.ix = None
142
143 self._setupContextDispatchers()
144
145 split = qt.QSplitter(self)
146 split.setOrientation(qt.QSplitter.Vertical)
147
148 lv = self.listview = ModelObjectListView(split)
149
150
151
152 lv.setRootIsDecorated(True)
153
154
155 self.connect( lv, qt.SIGNAL("selectionChanged()"), self._itemSelected )
156 self.connect( lv, qt.SIGNAL("rightButtonPressed(QListViewItem *,const QPoint&,int)"), self._showContextMenu )
157
158 lv.addColumn("Hosts")
159 lv.setColumnWidthMode(0,qt.QListView.Maximum)
160
161
162
163 lv.setTreeStepSize(20)
164
165
166
167
168
169 self.rootitem = None
170
171
172
173
174
175 self.details_table = EditionTable(split)
176 hbox = qt.QHBox(self)
177 self.object_label = qt.QLabel("", hbox)
178 self.object_label.setMinimumSize(qt.QSize(50, 25))
179 self.save_button = SaveButton(hbox, self._item_save)
180 self._save_callback = None
181
182 self.prefchilds = []
183
184
185
186
187 def load(self, workspace, workspace_type):
188 self.rootitem = WorkspaceListViewItem(self.listview, workspace, workspace_type)
189 self.listview.setSelected(self.rootitem, True)
190
191 def update(self, hosts):
192 self.clearTree()
193 self.redrawTree(hosts)
194
195 def sizeHint(self):
196 """Returns recommended size of dialog."""
197 return qt.QSize(70, 200)
198
199 def resizeEvent (self, event ):
200
201
202
203
204 self.listview.setColumnWidth(0,self.size().width()-7)
205
206
207
208 def clearTree(self):
209 """
210 clear the complete host tree
211 """
212 self._clearBranch(self.rootitem)
213 self._host_items.clear()
214
215 def _clearBranch(self, root):
216 """
217 clear a branch based on the root provided.
218 """
219 if root is not None:
220 i = root.firstChild()
221 items_to_remove = []
222 while i is not None:
223 items_to_remove.append(i)
224 i = i.nextSibling()
225
226 for i in items_to_remove:
227 if i.type == "Category":
228 self._delCategory(i.name)
229 elif i.type == "Host":
230 category_item = i.parent()
231 self._delHostFromCategory(i.object, category_item.name)
232
233 def redrawTree(self, hosts):
234 dialog = qt.QProgressDialog(self, "Loading workspace...", True)
235 dialog.setCaption("Please wait")
236 dialog.setLabelText("Loading workspace...")
237 dialog.setTotalSteps(len(hosts) + 1)
238 i = 1
239 for host in hosts:
240 dialog.setProgress(i)
241 category = host.getCurrentCategory()
242 self._addCategory(category)
243 self._addHostToCategory(host, category)
244 i += 1
245 for ci in self._category_items.values():
246 ci.setOpen(True)
247
248 self.createIndex()
249 dialog.setProgress(i)
250 self.filterTree(self._filter)
251 # we need to make sure that the dialog is closed
252 rem = dialog.totalSteps() - i
253 if rem > 0:
254 dialog.setProgress(i + rem)
255
256 def setReindex(self):
257 self.reindex_flag_lock.acquire()
258 if not self.reindex_flag:
259 self.reindex_flag = True
260 self.reindex_flag_lock.release()
261
262 def reindex(self):
263 self.reindex_flag_lock.acquire()
264 if self.reindex_flag:
265 self.createIndex()
266 self.reindex_flag = False
267 self.reindex_flag_lock.release()
268
269 def filterTree(self, mfilter=""):
270 self.reindex()
271 hosts=[]
272 viewall=False
273 self._filter=mfilter
274
275 for k in self._host_items.keys():
276 hosts.append(self._host_items[k].object)
277
278 if self._filter:
279 hosts=self._filterHost(hosts)
280 else:
281 viewall=True
282 hosts=[]
283
284
285
286
287 for k in self._host_items.keys():
288
289 if (self._host_items[k].object.name in hosts) or viewall==True:
290 self._host_items[k].setVisible(True)
291 else:
292 self._host_items[k].setVisible(False)
293
294
295 def _filterHost(self,hosts):
296
297
298 from whoosh.qparser import QueryParser
299 with self.ix.searcher() as searcher:
300 query = QueryParser("ip", self.ix.schema).parse(self._filter)
301 results = searcher.search(query, limit=None)
302
303
304
305 hostv={}
306 for r in results:
307 hostv[r['ip']]=1
308
309 return hostv
310
311 def createIndex(self):
312 hosts = self._model_controller.getAllHosts()
313 schema = Schema(ip=TEXT(stored=True),
314 hostname=TEXT(stored=True),
315 mac=TEXT(stored=True),
316 os=TEXT(stored=True),
317 port=TEXT(stored=True),
318 srvname=TEXT(stored=True),
319 srvstatus=TEXT(stored=True),
320 vulnn=TEXT(stored=True),
321 namen=TEXT(stored=True),
322 owned=BOOLEAN,
323 cred=BOOLEAN,
324 vuln=BOOLEAN,
325 note=BOOLEAN)
326
327 indexdir=CONF.getDataPath() + "/indexdir"
328 if not os.path.exists(indexdir):
329 os.mkdir(indexdir)
330
331 self.ix = create_in(indexdir, schema)
332 for host in hosts:
333 self.indexHost(host)
334
335 def indexHost(self, host):
336 writer = self.ix.writer()
337 writer.add_document(ip=unicode(host.name), os=unicode(host.getOS()),
338 owned=host.isOwned(),
339 vuln=True if host.vulnsCount() > 0 else False,
340 note=True if len(host.getNotes()) > 0 else False)
341
342 for i in host.getAllInterfaces():
343 for h in i._hostnames:
344 writer.add_document(ip=unicode(host.name),
345 hostname=unicode(h),
346 mac=unicode(i.getMAC()))
347
348 for v in host.getVulns():
349 writer.add_document(ip=unicode(host.name), vulnn=unicode(v.name))
350
351 for i in host.getAllInterfaces():
352 for s in i.getAllServices():
353 for v in s.getVulns():
354 writer.add_document(ip=unicode(host.name),
355 vulnn=unicode(v.name),
356 srvname=unicode(s.getName()))
357 for p in s.getPorts():
358 writer.add_document(
359 ip=unicode(host.name),
360 port=unicode(str(p)),
361 owned=s.isOwned(),
362 vuln=True if s.vulnsCount() > 0 else False,
363 note=True if len(s.getNotes()) > 0 else False,
364 cred=True if s.credsCount() > 0 else False,
365 srvname=unicode(s.getName()),
366 srvstatus=unicode(s.getStatus()))
367 writer.commit()
368
369 def removeIndexHost(self, host):
370 writer = self.ix.writer()
371 writer.delete_by_term('ip', host.name)
372 writer.commit()
373
374 def selectWord(self, word):
375 for k in self._host_items:
376 host_item = self._host_items[k]
377 if host_item.text(0).encode('utf8').strip() == word.strip():
378 self.listview.setSelected(host_item, True)
379 self.listview.ensureItemVisible(host_item)
380 break
381 else:
382 for i in host_item.object.getAllInterfaces():
383 if i.ipv4['address'] == word.strip():
384 self.listview.setSelected(host_item, True)
385 self.listview.ensureItemVisible(host_item)
386 break
387 elif i.ipv6['address'] == word.strip():
388 self.listview.setSelected(host_item, True)
389 self.listview.ensureItemVisible(host_item)
390 break
391 else:
392 for h in i.getHostnames():
393 if h == word.strip():
394 self.listview.setSelected(host_item, True)
395 self.listview.ensureItemVisible(host_item)
396 break
397
398 def workspaceChanged(self, workspace, workspace_type):
399 if self.rootitem:
400 root = self.rootitem
401 self.listview.takeItem(root)
402 del root
403 self.clearTree()
404 self.load(workspace,workspace_type)
405
406 def updateWorkspaceName(self, nconflicts):
407 self.rootitem.updateName(nconflicts)
408
409 def _resolveConflicts(self, item):
410 guiapi.resolveConflicts()
411
412 def showResolveConflictDialog(self, conflicts):
413 if len(conflicts):
414 dialog = ConflictResolutionDialog(conflicts)
415 dialog.exec_loop()
416
417 def _item_save(self):
418
419
420 if self._save_callback is not None:
421 self._save_callback()
422
423 def setSaveCallback(self, callback):
424 self._save_callback = callback
425
426 def _itemSelected(self, item=False):
427 """
428 this is called when a list view item is selected
429 """
430
431 i = self.listview.firstChild()
432 self.items_selected=[]
433 self.items_type={'Host': 0, 'Workspace': 0, 'Service':0,
434 'Interface':0, 'Application':0,'Category_General':0
435 ,'Category_Applications':0,'Category_Interfaces':0}
436 while i is not None:
437 if i.isSelected():
438
439 if i.type=="Category":
440 self.items_type[i.type+"_"+i.name] =self.items_type[i.type+"_"+i.name]+1
441 else:
442 self.items_type[i.type] =self.items_type[i.type]+1
443
444 self.items_selected.append(i)
445 i = i.itemBelow()
446 mtype={'Host': 0, 'Workspace': 0, 'Service':0, 'Interface':0, 'Application':0,'Category':0}
447
448 self.itemselected = self.listview.currentItem()
449
450
451 self.details_table.clear()
452 editor = self.itemselected.getEditor()
453 editor.fillEditionTable(self.details_table)
454 self.setSaveCallback(editor.save)
455
456 def getItemSelected(self):
457 return self.itemselected
458
459 def _addCategory(self, category):
460 if category not in self._category_tree:
461 self._category_tree[category] = []
462 ref = CategoryListViewItem(self.rootitem, category)
463 self._category_items[category] = ref
464 else:
465 ref = self._getCategoryListViewItem(category)
466 return ref
467
468 def _addHost(self, host):
469 category = host.getCurrentCategory()
470 self._addCategory(category)
471 self._addHostToCategory(host, category)
472 #self.removeIndexHost(host)
473 #self.indexHost(host)
474
475 def _removeHost(self, host_id):
476 item = self._host_items.get(host_id, None)
477 if host_id in self._host_items:
478 del self._host_items[host_id]
479 for category in self._category_tree.keys():
480 if host_id in self._category_tree.get(category):
481 self._category_tree[category].remove(host_id)
482 category_item = self._getCategoryListViewItem(category)
483 try:
484 category_item.takeItem(item)
485 except Exception:
486 api.devlog("Exception taking item from category")
487
488 def _editHost(self, host):
489 self._removeHost(host.getID())
490 self._addHost(host)
491
492 def _addHostToCategory(self, host, category):
493 category_item = self._addCategory(category)
494 self._host_items[host.getID()] = HostListViewItem(category_item, host.name, host)
495 self._category_tree[category].append(host.getID())
496
497 def _delHostFromCategory(self, host, category):
498 id = host.getID()
499 item = self._host_items.get(id, None)
500 if id in self._host_items:
501 del self._host_items[id]
502 if category in self._category_tree:
503 if id in self._category_tree[category]:
504 self._category_tree[category].remove(id)
505 category_item = self._getCategoryListViewItem(category)
506 api.devlog("_delHostFromCategory: about to call takeItem for category %s" % category)
507 try:
508 category_item.takeItem(item)
509 except Exception:
510 pass
511 api.devlog("_delHostFromCategory: after takeItem")
512
513 def _getCategoryListViewItem(self, category):
514 return self._category_items.get(category, None)
515
516 def _delCategory(self, category, recursive=False):
517 if category in self._category_tree:
518 if recursive:
519 for id in self._category_tree:
520 host_item = self._getHostListViewItem(id)
521 if host_item is not None:
522 self._delHostFromCategory(host_item.object, category)
523 else:
524
525
526 for id in self._category_tree:
527 host_item = self._getHostListViewItem(id)
528 if host_item is not None:
529 self._moveHostToCategory(host_item.object, CONF.getDefaultCategory())
530
531 del self._category_tree[category]
532 item = self._category_items[category]
533 del self._category_items[category]
534 self.rootitem.takeItem(item)
535
536 def _getHostListViewItem(self, id):
537 return self._host_items.get(id, None)
538
539 def _showContextMenu(self, item, pos, val):
540 """Pop up a context menu when an item is clicked on the list view."""
541 ret = None
542
543 if item is not None:
544
545
546
547
548 if self.items_type['Interface']:
549 if (self.items_type['Category_General'] or self.items_type['Workspace']):
550 popname="CategoryWorkspace_Interface"
551 elif (self.items_type['Host'] or self.items_type['Service']):
552 popname="ServiceHost_Interface"
553 else:
554 popname=item.type
555
556 elif (self.items_type['Host'] or self.items_type['Service']):
557 if (self.items_type['Category_General'] or self.items_type['Workspace']):
558 popname="CategoryWorkspace_ServiceHost"
559 elif (self.items_type['Host'] and self.items_type['Service']):
560 popname="Service_Host"
561 else:
562 if item.type is "Category":
563 popname="Host"
564 else:
565 popname=item.type
566 else:
567
568 if item.type is "Category":
569 popname=item.type + "_" + item.name
570 else:
571 popname=item.type
572
573 ret = self.contextpopups[popname].exec_loop(pos)
574
575 if ret in self.contextdispatchers:
576 self.contextdispatchers[ret](item)
577
578
579
580 api.devlog("contextMenuEvent - item: %s - ret %s" % (self.name, ret))
581
582
583
584 def _newHost(self, item):
585 api.devlog("newHost")
586 dialog = NewHostDialog(self, self._newHostCallback)
587 dialog.exec_loop()
588
589 def _newHostCallback(self, name, os):
590 if name:
591
592 guiapi.createAndAddHost(name, os=os)
593
594 def _delHost(self,item):
595 api.devlog("delHost")
596 if item is not None and item.object is not None:
597 dialog = MessageDialog(self,title="Host delete",callback=self._delSelectedCallback)
598 dialog.exec_loop()
599
600 def _delHostCallback(self, item):
601 api.devlog("delcallbackHost %s " % (item.object.name))
602 guiapi.delHost(item.object.getID())
603
604 def _newInterface(self, item):
605 api.devlog("newInterface")
606 dialog = NewInterfaceDialog(self, self._newInterfaceCallback)
607 dialog.exec_loop()
608
609 def _newInterfaceCallback(self, name, ipv4_address, ipv6_address):
610 if name and (ipv4_address or ipv6_address):
611 for i in self.items_selected:
612 host_id = i.object.getID()
613 guiapi.createAndAddInterface(host_id, name, ipv4_address=ipv4_address, ipv6_address=ipv6_address)
614
615 def _delInterface(self,item):
616 api.devlog("delInterface")
617 if item is not None and item.object is not None:
618 dialog = MessageDialog(self,title="Interface delete",callback=self._delSelectedCallback)
619 dialog.exec_loop()
620
621 def _delInterfaceCallback(self, item):
622 api.devlog("delcallbackInterface %s " % (item.object.name))
623 _parent=item.object.getParent()
624 guiapi.delInterface(_parent.getID(), item.object.getID())
625
626 def _newService(self,item):
627 api.devlog("newService")
628 dialog = NewServiceDialog(self, self._newServiceSelectedCallback)
629 dialog.exec_loop()
630
631 def _newServiceSelectedCallback(self, name, protocol, ports):
632 if name and protocol and ports:
633 for i in self.items_selected:
634 if i.type == "Interface":
635 interface_id = i.object.getID()
636 host_id = i.object.getParent().getID()
637 guiapi.createAndAddServiceToInterface(host_id, interface_id , name, protocol=protocol, ports=ports)
638
639 def _delService(self,item):
640 if item is not None and item.object is not None:
641 dialog = MessageDialog(self,title="Delete Item(s)",callback=self._delSelectedCallback)
642 dialog.exec_loop()
643
644 def _delServiceCallback(self, item):
645 api.devlog("delcallbackService %s " % (item.name))
646 _object=item.object
647 _host=_object.getParent()
648 guiapi.delServiceFromHost(_host.getID(), _object.getID())
649
650 def _delSelectedCallback(self,item):
651
652 for i in self.items_selected:
653 if i.type == "Host":
654 api.devlog("delcallbackHost %s " % (i.object.name))
655 guiapi.delHost(i.object.getID())
656 elif i.type == "Application":
657 api.devlog("delcallbackApplication %s " % (i.object.name))
658 _parent=i.object.getParent()
659 _object=i.object
660 guiapi.delApplication(_parent.getID(),_object.getID())
661 elif i.type == "Interface":
662 api.devlog("delcallbackInterface %s " % (i.object.name))
663 _parent=i.object.getParent()
664 _object=i.object
665 guiapi.delInterface(_parent.getID(), _object.getID())
666 elif i.type == "Service":
667 api.devlog("delcallbackService %s " % (i.name))
668 _object=i.object
669 parent_interface = self._getParentForType(i, "Interface").object
670 parent_host = self._getParentForType(i, "Host").object
671 guiapi.delServiceFromInterface(parent_host.getID(), parent_interface.getID(), _object.getID())
672
673 self.listview.setCurrentItem(self.rootitem)
674 self._itemSelected()
675
676 def _getParentForType(self, obj, obj_type):
677 parent = obj.parent()
678 if obj_type == parent.type:
679 return parent
680 else:
681 return self._getParentForType(parent, obj_type)
682
683 def _newCategory(self,item):
684 api.devlog("newCategory")
685
686 def _renCategory(self,item):
687 api.devlog("renCategory")
688
689 def _delCategorymenu(self,item):
690 api.devlog("delCategorymenu")
691 if item is not None:
692 dialog = MessageDialog(self,title="Category delete",callback=self._delCategoryCallback,item=item)
693 dialog.exec_loop()
694
695 def _delCategoryCallback(self, item):
696 api.devlog("delcallbackCategory %s " % (item.name))
697
698 def _newVuln(self, item):
699 api.devlog("newVuln")
700 if item is not None and item.object is not None:
701 vuln_web_enabled = False
702 if item.object.class_signature == "Service":
703 vuln_web_enabled = True
704 dialog = NewVulnDialog(
705 self,
706 callback=self._newVulnSelectedCallback,
707 vuln_web_enabled=vuln_web_enabled)
708 dialog.exec_loop()
709
710 def _newVulnSelectedCallback(self, *args):
711 callback = guiapi.createAndAddVuln
712 if args[0]:
713 # vuln web
714 callback = guiapi.createAndAddVulnWeb
715
716 for i in self.items_selected:
717 callback(i.object, *args[1:])
718
719 def _listVulns(self,item):
720 if item is not None and item.object is not None:
721 dialog = VulnsDialog(parent=self, model_object=item.object)
722 dialog.exec_loop()
723
724 def _listVulnsCvs(self,item):
725 vulns=""
726 hosts=[]
727 for k in self._host_items.keys():
728 hosts.append(self._host_items[k].object)
729
730 filename = qt.QFileDialog.getSaveFileName(
731 "/tmp",
732 "Vulnerability CVS (*.csv)",
733 None,
734 "save file dialog",
735 "Choose a file to save the vulns" )
736 from exporters.tofile import CSVVulnStatusReport
737 CSVVulnStatusReport(path = filename,
738 modelobjects = hosts).createCSVVulnStatusReport()
739
740 def _importVulnsCvs(self,item):
741 filename = qt.QFileDialog.getOpenFileName(
742 CONF.getDefaultTempPath(),
743 "Csv vulnerability file (*.*)",
744 None,
745 "open file dialog",
746 "Choose a vulnerability file" );
747
748 if os.path.isfile(filename):
749 with open(filename) as f:
750 data = f.read()
751 f.close()
752
753 for l in data.split("\n"):
754 api.devlog(l)
755 if re.search("^#",l):
756 api.devlog("ERROR FILE")
757 continue
758
759 d = l.split("|")
760
761 if len(d) <=5:
762 api.devlog("Error vuln line: ("+l+")" )
763 else:
764 self._newVulnImport(d[0],d[1],d[2],d[3],d[4],d[5],d[6])
765
766 def _newVulnImport(self,ip,port,protocol,name,desc,severity,type):
767 if port == "0": #vuln host
768 h_id = guiapi.createAndAddHost(ip)
769 v_id = guiapi.createAndAddVulnToHost(h_id, name, desc, [],severity)
770 else: #vuln port
771 h_id = guiapi.createAndAddHost(ip)
772 if self._isIPV4(ip):
773 i_id = guiapi.createAndAddInterface(h_id,ip,ipv4_address=ip)
774 else:
775 i_id = guiapi.createAndAddInterface(h_id,ip,ipv6_address=ip)
776 s_id = guiapi.createAndAddServiceToInterface(h_id,i_id,port,protocol,ports=[port])
777 if type == "2":
778 v_id = guiapi.createAndAddVulnWebToService(h_id,s_id, name, desc, "/","/",[],severity)
779 else:
780 v_id = guiapi.createAndAddVulnToService(h_id,s_id, name, desc, [],severity)
781
782 api.devlog("type:" + type)
783
784 def _isIPV4(self, ip):
785 if len(ip.split(".")) == 4:
786 return True
787 else:
788 return False
789
790 def _listNotes(self, item):
791 if item is not None and item.object is not None:
792 dialog = NotesDialog(parent=self, model_object=item.object)
793 dialog.exec_loop()
794
795 def _newNote(self, item):
796 if item is not None and item.object is not None:
797 dialog = NewNoteDialog(self, callback=self._newNoteSelectedCallback)
798 dialog.exec_loop()
799
800 def _newNoteSelectedCallback(self, name, text):
801 for i in self.items_selected:
802 if i.type == "Host":
803 api.devlog("newNotecallbackHost %s " % (i.object.name))
804 guiapi.createAndAddNoteToHost(i.object.getID(), name, text)
805 elif i.type == "Application":
806 _parent=i.object.getParent()
807 api.devlog("newNotecallbackApplication %s " % (i.object.name))
808 guiapi.createAndAddNoteToApplication(_parent.getID(), i.object.getID(), name, text)
809 elif i.type == "Interface":
810 _parent=i.object.getParent()
811 api.devlog("newNotecallbackInterface %s " % (i.object.name))
812 guiapi.createAndAddNoteToInterface(_parent.getID(), i.object.getID(), name, text)
813 elif i.type == "Service":
814 _parent=i.object.getParent().getParent()
815 api.devlog("newNotecallbackService %s " % (i.name))
816 guiapi.createAndAddNoteToService(_parent.getID(), i.object.getID(), name, text)
817
818 def _listCreds(self, item):
819 if item is not None and item.object is not None:
820 dialog = CredsDialog(parent=self, model_object=item.object)
821 dialog.exec_loop()
822
823 def _newCred(self, item):
824 api.devlog("newCred")
825 dialog = NewCredDialog(self, self._newCredSelectedCallback)
826 dialog.exec_loop()
827
828 def _importCreds(self, item):
829 filename = qt.QFileDialog.getOpenFileName(
830 CONF.getDefaultTempPath(),
831 "Csv user,pass or user:pass (*.*)",
832 None,
833 "open file dialog",
834 "Choose a password file" );
835
836 if os.path.isfile(filename):
837 with open(filename) as f:
838 data = f.read()
839 f.close()
840
841 for l in data.split():
842 api.devlog(l)
843 if re.search("^#",l):
844 api.devlog("ERROR FILE")
845 continue
846
847 d = l.split(",")
848 if len(d)<=1:
849 d = l.split(":")
850
851 api.devlog(d)
852 if len(d) <=1:
853 api.devlog("Error password line: ("+l+")" )
854 else:
855 self._newCredSelectedCallback(d[0],d[1])
856
857 def _newCredSelectedCallback(self,username,password):
858
859 for i in self.items_selected:
860 if i.type in ['Host','Service']:
861 guiapi.createAndAddCred(i.object,username,password)
862
863 def _showWorkspaceProperties(self, item):
864 if item.object is not None:
865 d = WorkspacePropertiesDialog(self, "Workspace Properties", workspace=item.object)
866 d.exec_loop()
867
868 def _modelObjectViewUpdater(self):
869 if len(self.__pendingModelObjectRedraws):
870 self.update(self.__pendingModelObjectRedraws.pop().hosts)
871 self.__pendingModelObjectRedraws[:] = []
872
873 def customEvent(self, event):
874 if event.type() == UPDATEMODEL_ID:
875 self.__pendingModelObjectRedraws.append(event)
876
877 elif event.type() == RENAMEHOSTSROOT_ID:
878 self.renameRootItem(event.name)
879
880 elif event.type() == DIFFHOSTS_ID:
881 self._diffHosts(event.old_host, event.new_host)
882
883 elif event.type() == CLEARHOSTS_ID:
884 self.clearTree()
885
886 elif event.type() == WORKSPACE_CHANGED:
887 self.workspaceChanged(event.workspace, event.workspace_type)
888
889 elif event.type() == CONFLICT_UPDATE:
890 self.updateWorkspaceName(event.nconflicts)
891
892 elif event.type() == RESOLVECONFLICTS_ID:
893 self.showResolveConflictDialog(event.conflicts)
894
895 elif event.type() == ADDHOST:
896 self._addHost(event.host)
897 self.setReindex()
898
899 elif event.type() == DELHOST:
900 self._removeHost(event.host_id)
901 self.setReindex()
902
903 elif event.type() == EDITHOST:
904 self._editHost(event.host)
905 self.setReindex()
906
907
908 def _setupContextPopups(self):
909 """
910 Configures a context popup menu for each kind of item shown in the tree.
911 This is done because different options may be needed for each item
912 """
913
914 popup = qt.QPopupMenu(self)
915
916
917
918 popup.insertSeparator()
919 popup.insertItem('Resolve Conflicts', 303)
920 popup.insertItem('Save Vulns CSV', 402)
921 popup.insertItem('Import Vulns CSV', 403)
922
923
924 popup.insertSeparator()
925 popup.insertItem('Add Host', 800)
926
927 self.contextpopups["Workspace"] = popup
928
929 self.contextpopups["Category_General"] = self.contextpopups["Workspace"]
930
931
932 popup = qt.QPopupMenu(self)
933
934
935
936
937 self.contextpopups["Category_Applications"] = popup
938
939
940 popup = qt.QPopupMenu(self)
941 popup.insertItem('Add Interfaces', 600)
942
943
944
945 self.contextpopups["Category_Interfaces"] = popup
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964 popup = qt.QPopupMenu(self)
965 popup.insertItem('Delete Host', 802)
966 popup.insertSeparator()
967 popup.insertItem('Add Interface', 600)
968 popup.insertSeparator()
969 popup.insertItem('New Vulnerability',400)
970 popup.insertItem('List Vulnerabilities',401)
971 popup.insertSeparator()
972 popup.insertItem('New note', 500)
973 popup.insertItem('Show notes', 501)
974 popup.insertSeparator()
975 popup.insertItem('New Credential', 550)
976 popup.insertItem('Show Credentials', 551)
977 popup.insertItem('Import Creds', 561)
978
979
980
981 self.contextpopups["Host"] = popup
982
983
984 popup = qt.QPopupMenu(self)
985 popup.insertItem('Delete Interface', 602)
986 popup.insertSeparator()
987 popup.insertItem('Add Service', 200)
988 popup.insertSeparator()
989 popup.insertItem('New Vulnerability',400)
990 popup.insertItem('List Vulnerabilities',401)
991 popup.insertSeparator()
992 popup.insertItem('New note', 500)
993 popup.insertItem('Show notes', 501)
994
995
996
997 self.contextpopups["Interface"] = popup
998
999
1000 popup = qt.QPopupMenu(self)
1001 popup.insertItem('Delete Service', 202)
1002 popup.insertSeparator()
1003 popup.insertItem('New Vulnerability',400)
1004 popup.insertItem('List Vulnerabilities',401)
1005 popup.insertSeparator()
1006 popup.insertItem('New note', 500)
1007 popup.insertItem('Show notes', 501)
1008 popup.insertSeparator()
1009 popup.insertItem('New Credential', 550)
1010 popup.insertItem('Show Credentials', 551)
1011 popup.insertItem('Import Creds', 561)
1012
1013
1014
1015 self.contextpopups["Service"] = popup
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025 popup = qt.QPopupMenu(self)
1026 popup.insertItem('Delete Items', 202)
1027 popup.insertSeparator()
1028 popup.insertItem('New Vulnerability Items',400)
1029 popup.insertSeparator()
1030 popup.insertItem('New note Items', 500)
1031 popup.insertSeparator()
1032 popup.insertItem('New Credential', 550)
1033 popup.insertItem('Import Creds', 561)
1034
1035 self.contextpopups["Service_Host"] = popup
1036
1037
1038
1039 popup = qt.QPopupMenu(self)
1040 popup.insertItem('Add Service', 200)
1041 popup.insertSeparator()
1042 popup.insertItem('Delete Items', 202)
1043 popup.insertSeparator()
1044 popup.insertItem('New Vulnerability Items',400)
1045 popup.insertSeparator()
1046 popup.insertItem('New note Items', 500)
1047 popup.insertSeparator()
1048 popup.insertItem('New Credential', 550)
1049 popup.insertItem('Import Creds', 561)
1050
1051
1052
1053 self.contextpopups["ServiceHost_Interface"] = popup
1054
1055
1056 popup = qt.QPopupMenu(self)
1057
1058
1059 popup.insertItem('Properties', 302)
1060 popup.insertSeparator()
1061 popup.insertItem('Add Host', 800)
1062 popup.insertSeparator()
1063 popup.insertItem('Add Service', 200)
1064 popup.insertSeparator()
1065 popup.insertItem('Delete Items', 202)
1066 popup.insertSeparator()
1067 popup.insertItem('New Vulnerability Items',400)
1068 popup.insertSeparator()
1069 popup.insertItem('New note Items', 500)
1070 popup.insertSeparator()
1071 popup.insertItem('Add Service', 200)
1072 popup.insertSeparator()
1073 popup.insertItem('New Credential', 550)
1074 popup.insertItem('Import Creds', 561)
1075
1076
1077 self.contextpopups["CategoryWorkspace_Interface"] = popup
1078
1079
1080 popup = qt.QPopupMenu(self)
1081
1082
1083 popup.insertItem('Properties', 302)
1084 popup.insertSeparator()
1085 popup.insertItem('Add Host', 800)
1086 popup.insertSeparator()
1087 popup.insertItem('Delete Items', 202)
1088 popup.insertSeparator()
1089 popup.insertItem('New Vulnerability Items',400)
1090 popup.insertSeparator()
1091 popup.insertItem('New note Items', 500)
1092 popup.insertSeparator()
1093 popup.insertItem('New Credential', 550)
1094 popup.insertItem('Import Creds', 561)
1095
1096
1097 self.contextpopups["CategoryWorkspace_ServiceHost"] = popup
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108 def _setupContextDispatchers(self):
1109 """
1110 Configures a context dispatcher for each kind of item shown in the tree.
1111 This is done because different options may be needed for each item
1112 """
1113
1114 self.contextdispatchers[100] = self._newCategory
1115 self.contextdispatchers[102] = self._delCategorymenu
1116
1117 self.contextdispatchers[200] = self._newService
1118 self.contextdispatchers[202] = self._delService
1119
1120
1121
1122 self.contextdispatchers[302] = self._showWorkspaceProperties
1123 self.contextdispatchers[303] = self._resolveConflicts
1124
1125 self.contextdispatchers[400] = self._newVuln
1126 self.contextdispatchers[401] = self._listVulns
1127 self.contextdispatchers[402] = self._listVulnsCvs
1128 self.contextdispatchers[403] = self._importVulnsCvs
1129
1130 self.contextdispatchers[500] = self._newNote
1131 self.contextdispatchers[501] = self._listNotes
1132
1133 self.contextdispatchers[550] = self._newCred
1134 self.contextdispatchers[551] = self._listCreds
1135 self.contextdispatchers[561] = self._importCreds
1136
1137 self.contextdispatchers[600] = self._newInterface
1138 self.contextdispatchers[602] = self._delInterface
1139
1140 self.contextdispatchers[800] = self._newHost
1141 self.contextdispatchers[802] = self._delHost
1142
1143
1144 def renameRootItem(self, new_name):
1145 self.rootitem.setText(0, new_name)
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 import qt
7 from threading import Lock
8 from gui.qt3.modelobjectitems import *
9 import model.api as api
10 import model.guiapi as guiapi
11 import re as re
12 from gui.qt3.dialogs import NewVulnDialog, ConflictResolutionDialog, MessageDialog, NotesDialog, VulnsDialog, CredsDialog
13 from gui.qt3.customevents import *
14 from gui.qt3.dialogs import WorkspacePropertiesDialog
15 from gui.qt3.edition import EditionTable, NewServiceDialog, NewHostDialog, NewInterfaceDialog, NewCredDialog, NewNoteDialog
16
17 from config.configuration import getInstanceConfiguration
18 CONF = getInstanceConfiguration()
19
20 from whoosh.index import create_in
21 from whoosh.fields import *
22
23
24 class PropertyItem(qt.QListViewItem):
25
26
27 """Item for displaying a preferences-set in HostsBrowser."""
28 def __init__(self, settings, number, parent):
29 """_plugin_settings is the _plugin_settings class to work for
30 parent is the parent ListViewItem (of type ModelObjectListViewItem)
31 """
32 qt.QListViewItem.__init__(self, parent)
33 self.settings = settings
34 self.parent = parent
35 self.widget = None
36 self.setText(0, settings.name)
37 self.setText(1, 'setting')
38 self.index = number
39
40 def compare(self, i, col, ascending):
41 """Always sort according to the index value."""
42
43 a = [-1, 1][ascending]
44
45 if self.index < i.index:
46 return -1*a
47 elif self.index > i.index:
48 return 1*a
49 else:
50 return 0
51
52
53 class ModelObjectListView(qt.QListView):
54 """
55 List view for hosts
56 It allows Drag and Drop (TODO)
57 """
58 def __init__(self, parent=None):
59 qt.QListView.__init__(self, parent)
60 self.setSelectionMode(qt.QListView.Extended)
61
62
63
64
65
66
67
68 def selectWidget(self, widget):
69 """Find the widget in the list and select it."""
70
71
72
73 iter = qt.QListViewItemIterator(self)
74
75 found = None
76 while True:
77 item = iter.current()
78 if item == None:
79 break
80 if item.widget == widget:
81 found = item
82 break
83 iter += 1
84
85 if found:
86 self.ensureItemVisible(found)
87 self.setSelected(found, True)
88
89 def sizeHint(self):
90 return qt.QSize(500, 800)
91
92
93 class SaveButton(qt.QPushButton):
94 def __init__(self, parent, callback):
95 qt.QPushButton.__init__(self, "Save", parent)
96 parent.connect(self, qt.SIGNAL('clicked()'), callback)
97 self.setMaximumSize(qt.QSize(75, 25))
98
99
100 class HostsBrowser(qt.QVBox):
101 """Tree view to display Hosts"""
102
103 def __init__(self, parent, model_controller, caption):
104 qt.QVBox.__init__(self, parent)
105
106 self._model_controller = model_controller
107
108 self.modelUpdateTimer = qt.QTimer(self)
109
110 self.__pendingModelObjectRedraws = []
111
112 self.reindex_flag_lock = Lock()
113 self.reindex_flag = False
114
115 self.connect( self.modelUpdateTimer, qt.SIGNAL("timeout()"), self._modelObjectViewUpdater)
116
117 self.modelUpdateTimer.start( 1000 , False)
118
119
120 self.setName(caption) if caption else self.setName("")
121
122 self.setFrameStyle(qt.QFrame.Panel | qt.QFrame.Plain)
123 self.setLineWidth(0)
124
125 self._host_items = {}
126
127
128 self._category_items = {}
129
130
131
132 self._category_tree = {}
133
134 self.contextpopups = {}
135 self._setupContextPopups()
136
137 self.contextdispatchers = {}
138
139
140 self._filter = ""
141 self.ix = None
142
143 self._setupContextDispatchers()
144
145 split = qt.QSplitter(self)
146 split.setOrientation(qt.QSplitter.Vertical)
147
148 lv = self.listview = ModelObjectListView(split)
149
150
151
152 lv.setRootIsDecorated(True)
153
154
155 self.connect( lv, qt.SIGNAL("selectionChanged()"), self._itemSelected )
156 self.connect( lv, qt.SIGNAL("rightButtonPressed(QListViewItem *,const QPoint&,int)"), self._showContextMenu )
157
158 lv.addColumn("Hosts")
159 lv.setColumnWidthMode(0,qt.QListView.Maximum)
160
161
162
163 lv.setTreeStepSize(20)
164
165
166
167
168
169 self.rootitem = None
170
171
172
173
174
175 self.details_table = EditionTable(split)
176 hbox = qt.QHBox(self)
177 self.object_label = qt.QLabel("", hbox)
178 self.object_label.setMinimumSize(qt.QSize(50, 25))
179 self.save_button = SaveButton(hbox, self._item_save)
180 self._save_callback = None
181
182 self.prefchilds = []
183
184
185
186
187 def load(self, workspace, workspace_type):
188 self.rootitem = WorkspaceListViewItem(self.listview, workspace, workspace_type)
189 self.listview.setSelected(self.rootitem, True)
190
191 def update(self, hosts):
192 self.clearTree()
193 self.redrawTree(hosts)
194
195 def sizeHint(self):
196 """Returns recommended size of dialog."""
197 return qt.QSize(70, 200)
198
199 def resizeEvent (self, event ):
200
201
202
203
204 self.listview.setColumnWidth(0,self.size().width()-7)
205
206
207
208 def clearTree(self):
209 """
210 clear the complete host tree
211 """
212 self._clearBranch(self.rootitem)
213 self._host_items.clear()
214
215 def _clearBranch(self, root):
216 """
217 clear a branch based on the root provided.
218 """
219 if root is not None:
220 i = root.firstChild()
221 items_to_remove = []
222 while i is not None:
223 items_to_remove.append(i)
224 i = i.nextSibling()
225
226 for i in items_to_remove:
227 if i.type == "Category":
228 self._delCategory(i.name)
229 elif i.type == "Host":
230 category_item = i.parent()
231 self._delHostFromCategory(i.object, category_item.name)
232
233 def redrawTree(self, hosts):
234 dialog = qt.QProgressDialog(self, "Loading workspace...", True)
235 dialog.setCaption("Please wait")
236 dialog.setLabelText("Loading workspace...")
237 dialog.setTotalSteps(len(hosts) + 1)
238 i = 1
239 for host in hosts:
240 dialog.setProgress(i)
241 category = host.getCurrentCategory()
242 self._addCategory(category)
243 self._addHostToCategory(host, category)
244 i += 1
245 for ci in self._category_items.values():
246 ci.setOpen(True)
247
248 self.createIndex()
249 dialog.setProgress(i)
250 self.filterTree(self._filter)
251 # we need to make sure that the dialog is closed
252 rem = dialog.totalSteps() - i
253 if rem > 0:
254 dialog.setProgress(i + rem)
255
256 def setReindex(self):
257 self.reindex_flag_lock.acquire()
258 if not self.reindex_flag:
259 self.reindex_flag = True
260 self.reindex_flag_lock.release()
261
262 def reindex(self):
263 self.reindex_flag_lock.acquire()
264 if self.reindex_flag:
265 self.createIndex()
266 self.reindex_flag = False
267 self.reindex_flag_lock.release()
268
269 def filterTree(self, mfilter=""):
270 self.reindex()
271 hosts=[]
272 viewall=False
273 self._filter=mfilter
274
275 for k in self._host_items.keys():
276 hosts.append(self._host_items[k].object)
277
278 if self._filter:
279 hosts=self._filterHost(hosts)
280 else:
281 viewall=True
282 hosts=[]
283
284
285
286
287 for k in self._host_items.keys():
288
289 if (self._host_items[k].object.name in hosts) or viewall==True:
290 self._host_items[k].setVisible(True)
291 else:
292 self._host_items[k].setVisible(False)
293
294
295 def _filterHost(self,hosts):
296
297
298 from whoosh.qparser import QueryParser
299 with self.ix.searcher() as searcher:
300 query = QueryParser("ip", self.ix.schema).parse(self._filter)
301 results = searcher.search(query, limit=None)
302
303
304
305 hostv={}
306 for r in results:
307 hostv[r['ip']]=1
308
309 return hostv
310
311 def createIndex(self):
312 hosts = self._model_controller.getAllHosts()
313 schema = Schema(ip=TEXT(stored=True),
314 hostname=TEXT(stored=True),
315 mac=TEXT(stored=True),
316 os=TEXT(stored=True),
317 port=TEXT(stored=True),
318 srvname=TEXT(stored=True),
319 srvstatus=TEXT(stored=True),
320 vulnn=TEXT(stored=True),
321 namen=TEXT(stored=True),
322 owned=BOOLEAN,
323 cred=BOOLEAN,
324 vuln=BOOLEAN,
325 note=BOOLEAN)
326
327 indexdir=CONF.getDataPath() + "/indexdir"
328 if not os.path.exists(indexdir):
329 os.mkdir(indexdir)
330
331 self.ix = create_in(indexdir, schema)
332 for host in hosts:
333 self.indexHost(host)
334
335 def indexHost(self, host):
336 writer = self.ix.writer()
337 writer.add_document(ip=unicode(host.name), os=unicode(host.getOS()),
338 owned=host.isOwned(),
339 vuln=True if host.vulnsCount() > 0 else False,
340 note=True if len(host.getNotes()) > 0 else False)
341
342 for i in host.getAllInterfaces():
343 for h in i._hostnames:
344 writer.add_document(ip=unicode(host.name),
345 hostname=unicode(h),
346 mac=unicode(i.getMAC()))
347
348 for v in host.getVulns():
349 writer.add_document(ip=unicode(host.name), vulnn=unicode(v.name))
350
351 for i in host.getAllInterfaces():
352 for s in i.getAllServices():
353 for v in s.getVulns():
354 writer.add_document(ip=unicode(host.name),
355 vulnn=unicode(v.name),
356 srvname=unicode(s.getName()))
357 for p in s.getPorts():
358 writer.add_document(
359 ip=unicode(host.name),
360 port=unicode(str(p)),
361 owned=s.isOwned(),
362 vuln=True if s.vulnsCount() > 0 else False,
363 note=True if len(s.getNotes()) > 0 else False,
364 cred=True if s.credsCount() > 0 else False,
365 srvname=unicode(s.getName()),
366 srvstatus=unicode(s.getStatus()))
367 writer.commit()
368
369 def removeIndexHost(self, host):
370 writer = self.ix.writer()
371 writer.delete_by_term('ip', host.name)
372 writer.commit()
373
374 def selectWord(self, word):
375 for k in self._host_items:
376 host_item = self._host_items[k]
377 if host_item.text(0).encode('utf8').strip() == word.strip():
378 self.listview.setSelected(host_item, True)
379 self.listview.ensureItemVisible(host_item)
380 break
381 else:
382 for i in host_item.object.getAllInterfaces():
383 if i.ipv4['address'] == word.strip():
384 self.listview.setSelected(host_item, True)
385 self.listview.ensureItemVisible(host_item)
386 break
387 elif i.ipv6['address'] == word.strip():
388 self.listview.setSelected(host_item, True)
389 self.listview.ensureItemVisible(host_item)
390 break
391 else:
392 for h in i.getHostnames():
393 if h == word.strip():
394 self.listview.setSelected(host_item, True)
395 self.listview.ensureItemVisible(host_item)
396 break
397
398 def workspaceChanged(self, workspace, workspace_type):
399 if self.rootitem:
400 root = self.rootitem
401 self.listview.takeItem(root)
402 del root
403 self.clearTree()
404 self.load(workspace,workspace_type)
405
406 def updateWorkspaceName(self, nconflicts):
407 self.rootitem.updateName(nconflicts)
408
409 def _resolveConflicts(self, item):
410 guiapi.resolveConflicts()
411
412 def showResolveConflictDialog(self, conflicts):
413 if len(conflicts):
414 dialog = ConflictResolutionDialog(conflicts)
415 dialog.exec_loop()
416
417 def _item_save(self):
418
419
420 if self._save_callback is not None:
421 self._save_callback()
422
423 def setSaveCallback(self, callback):
424 self._save_callback = callback
425
426 def _itemSelected(self, item=False):
427 """
428 this is called when a list view item is selected
429 """
430
431 i = self.listview.firstChild()
432 self.items_selected=[]
433 self.items_type={'Host': 0, 'Workspace': 0, 'Service':0,
434 'Interface':0, 'Application':0,'Category_General':0
435 ,'Category_Applications':0,'Category_Interfaces':0}
436 while i is not None:
437 if i.isSelected():
438
439 if i.type=="Category":
440 self.items_type[i.type+"_"+i.name] =self.items_type[i.type+"_"+i.name]+1
441 else:
442 self.items_type[i.type] =self.items_type[i.type]+1
443
444 self.items_selected.append(i)
445 i = i.itemBelow()
446 mtype={'Host': 0, 'Workspace': 0, 'Service':0, 'Interface':0, 'Application':0,'Category':0}
447
448 self.itemselected = self.listview.currentItem()
449
450
451 self.details_table.clear()
452 editor = self.itemselected.getEditor()
453 editor.fillEditionTable(self.details_table)
454 self.setSaveCallback(editor.save)
455
456 def getItemSelected(self):
457 return self.itemselected
458
459 def _addCategory(self, category):
460 if category not in self._category_tree:
461 self._category_tree[category] = []
462 ref = CategoryListViewItem(self.rootitem, category)
463 self._category_items[category] = ref
464 else:
465 ref = self._getCategoryListViewItem(category)
466 return ref
467
468 def _addHost(self, host):
469 category = host.getCurrentCategory()
470 self._addCategory(category)
471 self._addHostToCategory(host, category)
472 #self.removeIndexHost(host)
473 #self.indexHost(host)
474
475 def _removeHost(self, host_id):
476 item = self._host_items.get(host_id, None)
477 if host_id in self._host_items:
478 del self._host_items[host_id]
479 for category in self._category_tree.keys():
480 if host_id in self._category_tree.get(category):
481 self._category_tree[category].remove(host_id)
482 category_item = self._getCategoryListViewItem(category)
483 try:
484 category_item.takeItem(item)
485 except Exception:
486 api.devlog("Exception taking item from category")
487
488 def _editHost(self, host):
489 self._removeHost(host.getID())
490 self._addHost(host)
491
492 def _addHostToCategory(self, host, category):
493 category_item = self._addCategory(category)
494 self._host_items[host.getID()] = HostListViewItem(category_item, host.name, host)
495 self._category_tree[category].append(host.getID())
496
497 def _delHostFromCategory(self, host, category):
498 id = host.getID()
499 item = self._host_items.get(id, None)
500 if id in self._host_items:
501 del self._host_items[id]
502 if category in self._category_tree:
503 if id in self._category_tree[category]:
504 self._category_tree[category].remove(id)
505 category_item = self._getCategoryListViewItem(category)
506 api.devlog("_delHostFromCategory: about to call takeItem for category %s" % category)
507 try:
508 category_item.takeItem(item)
509 except Exception:
510 pass
511 api.devlog("_delHostFromCategory: after takeItem")
512
513 def _getCategoryListViewItem(self, category):
514 return self._category_items.get(category, None)
515
516 def _delCategory(self, category, recursive=False):
517 if category in self._category_tree:
518 if recursive:
519 for id in self._category_tree:
520 host_item = self._getHostListViewItem(id)
521 if host_item is not None:
522 self._delHostFromCategory(host_item.object, category)
523 else:
524
525
526 for id in self._category_tree:
527 host_item = self._getHostListViewItem(id)
528 if host_item is not None:
529 self._moveHostToCategory(host_item.object, CONF.getDefaultCategory())
530
531 del self._category_tree[category]
532 item = self._category_items[category]
533 del self._category_items[category]
534 self.rootitem.takeItem(item)
535
536 def _getHostListViewItem(self, id):
537 return self._host_items.get(id, None)
538
539 def _showContextMenu(self, item, pos, val):
540 """Pop up a context menu when an item is clicked on the list view."""
541 ret = None
542
543 if item is not None:
544
545
546
547
548 if self.items_type['Interface']:
549 if (self.items_type['Category_General'] or self.items_type['Workspace']):
550 popname="CategoryWorkspace_Interface"
551 elif (self.items_type['Host'] or self.items_type['Service']):
552 popname="ServiceHost_Interface"
553 else:
554 popname=item.type
555
556 elif (self.items_type['Host'] or self.items_type['Service']):
557 if (self.items_type['Category_General'] or self.items_type['Workspace']):
558 popname="CategoryWorkspace_ServiceHost"
559 elif (self.items_type['Host'] and self.items_type['Service']):
560 popname="Service_Host"
561 else:
562 if item.type is "Category":
563 popname="Host"
564 else:
565 popname=item.type
566 else:
567
568 if item.type is "Category":
569 popname=item.type + "_" + item.name
570 else:
571 popname=item.type
572
573 ret = self.contextpopups[popname].exec_loop(pos)
574
575 if ret in self.contextdispatchers:
576 self.contextdispatchers[ret](item)
577
578
579
580 api.devlog("contextMenuEvent - item: %s - ret %s" % (self.name, ret))
581
582
583
584 def _newHost(self, item):
585 api.devlog("newHost")
586 dialog = NewHostDialog(self, self._newHostCallback)
587 dialog.exec_loop()
588
589 def _newHostCallback(self, name, os):
590 if name:
591
592 guiapi.createAndAddHost(name, os=os)
593
594 def _delHost(self,item):
595 api.devlog("delHost")
596 if item is not None and item.object is not None:
597 dialog = MessageDialog(self,title="Host delete",callback=self._delSelectedCallback)
598 dialog.exec_loop()
599
600 def _delHostCallback(self, item):
601 api.devlog("delcallbackHost %s " % (item.object.name))
602 guiapi.delHost(item.object.getID())
603
604 def _newInterface(self, item):
605 api.devlog("newInterface")
606 dialog = NewInterfaceDialog(self, self._newInterfaceCallback)
607 dialog.exec_loop()
608
609 def _newInterfaceCallback(self, name, ipv4_address, ipv6_address):
610 if name and (ipv4_address or ipv6_address):
611 for i in self.items_selected:
612 host_id = i.object.getID()
613 guiapi.createAndAddInterface(host_id, name, ipv4_address=ipv4_address, ipv6_address=ipv6_address)
614
615 def _delInterface(self,item):
616 api.devlog("delInterface")
617 if item is not None and item.object is not None:
618 dialog = MessageDialog(self,title="Interface delete",callback=self._delSelectedCallback)
619 dialog.exec_loop()
620
621 def _delInterfaceCallback(self, item):
622 api.devlog("delcallbackInterface %s " % (item.object.name))
623 _parent=item.object.getParent()
624 guiapi.delInterface(_parent.getID(), item.object.getID())
625
626 def _newService(self,item):
627 api.devlog("newService")
628 dialog = NewServiceDialog(self, self._newServiceSelectedCallback)
629 dialog.exec_loop()
630
631 def _newServiceSelectedCallback(self, name, protocol, ports):
632 if name and protocol and ports:
633 for i in self.items_selected:
634 if i.type == "Interface":
635 interface_id = i.object.getID()
636 host_id = i.object.getParent().getID()
637 guiapi.createAndAddServiceToInterface(host_id, interface_id , name, protocol=protocol, ports=ports)
638
639 def _delService(self,item):
640 if item is not None and item.object is not None:
641 dialog = MessageDialog(self,title="Delete Item(s)",callback=self._delSelectedCallback)
642 dialog.exec_loop()
643
644 def _delServiceCallback(self, item):
645 api.devlog("delcallbackService %s " % (item.name))
646 _object=item.object
647 _host=_object.getParent()
648 guiapi.delServiceFromHost(_host.getID(), _object.getID())
649
650 def _delSelectedCallback(self,item):
651
652 for i in self.items_selected:
653 if i.type == "Host":
654 api.devlog("delcallbackHost %s " % (i.object.name))
655 guiapi.delHost(i.object.getID())
656 elif i.type == "Application":
657 api.devlog("delcallbackApplication %s " % (i.object.name))
658 _parent=i.object.getParent()
659 _object=i.object
660 guiapi.delApplication(_parent.getID(),_object.getID())
661 elif i.type == "Interface":
662 api.devlog("delcallbackInterface %s " % (i.object.name))
663 _parent=i.object.getParent()
664 _object=i.object
665 guiapi.delInterface(_parent.getID(), _object.getID())
666 elif i.type == "Service":
667 api.devlog("delcallbackService %s " % (i.name))
668 _object=i.object
669 parent_interface = self._getParentForType(i, "Interface").object
670 parent_host = self._getParentForType(i, "Host").object
671 guiapi.delServiceFromInterface(parent_host.getID(), parent_interface.getID(), _object.getID())
672
673 self.listview.setCurrentItem(self.rootitem)
674 self._itemSelected()
675
676 def _getParentForType(self, obj, obj_type):
677 parent = obj.parent()
678 if obj_type == parent.type:
679 return parent
680 else:
681 return self._getParentForType(parent, obj_type)
682
683 def _newCategory(self,item):
684 api.devlog("newCategory")
685
686 def _renCategory(self,item):
687 api.devlog("renCategory")
688
689 def _delCategorymenu(self,item):
690 api.devlog("delCategorymenu")
691 if item is not None:
692 dialog = MessageDialog(self,title="Category delete",callback=self._delCategoryCallback,item=item)
693 dialog.exec_loop()
694
695 def _delCategoryCallback(self, item):
696 api.devlog("delcallbackCategory %s " % (item.name))
697
698 def _newVuln(self, item):
699 api.devlog("newVuln")
700 if item is not None and item.object is not None:
701 vuln_web_enabled = False
702 if item.object.class_signature == "Service":
703 vuln_web_enabled = True
704 dialog = NewVulnDialog(
705 self,
706 callback=self._newVulnSelectedCallback,
707 vuln_web_enabled=vuln_web_enabled)
708 dialog.exec_loop()
709
710 def _newVulnSelectedCallback(self, *args):
711 callback = guiapi.createAndAddVuln
712 if args[0]:
713 # vuln web
714 callback = guiapi.createAndAddVulnWeb
715
716 for i in self.items_selected:
717 callback(i.object, *args[1:])
718
719 def _listVulns(self,item):
720 if item is not None and item.object is not None:
721 dialog = VulnsDialog(parent=self, model_object=item.object)
722 dialog.exec_loop()
723
724 def _listVulnsCvs(self,item):
725 vulns=""
726 hosts=[]
727 for k in self._host_items.keys():
728 hosts.append(self._host_items[k].object)
729
730 filename = qt.QFileDialog.getSaveFileName(
731 "/tmp",
732 "Vulnerability CVS (*.csv)",
733 None,
734 "save file dialog",
735 "Choose a file to save the vulns" )
736 from exporters.tofile import CSVVulnStatusReport
737 CSVVulnStatusReport(path = filename,
738 modelobjects = hosts).createCSVVulnStatusReport()
739
740 def _importVulnsCvs(self,item):
741 filename = qt.QFileDialog.getOpenFileName(
742 CONF.getDefaultTempPath(),
743 "Csv vulnerability file (*.*)",
744 None,
745 "open file dialog",
746 "Choose a vulnerability file" );
747
748 if os.path.isfile(filename):
749 with open(filename) as f:
750 data = f.read()
751 f.close()
752
753 for l in data.split("\n"):
754 api.devlog(l)
755 if re.search("^#",l):
756 api.devlog("ERROR FILE")
757 continue
758
759 d = l.split("|")
760
761 if len(d) <=8:
762 api.log("Error vuln line: ("+l+")" )
763 else:
764 self._newVulnImport(d[1],d[2],d[3],d[4],d[5],d[6],d[7])
765
766 def _newVulnImport(self,ip,port,protocol,name,desc,severity,type):
767 if port == "": #vuln host
768 h_id = guiapi.createAndAddHost(ip)
769 v_id = guiapi.createAndAddVulnToHost(h_id, name, desc, [],severity)
770 else: #vuln port
771 h_id = guiapi.createAndAddHost(ip)
772 if self._isIPV4(ip):
773 i_id = guiapi.createAndAddInterface(h_id,ip,ipv4_address=ip)
774 else:
775 i_id = guiapi.createAndAddInterface(h_id,ip,ipv6_address=ip)
776 s_id = guiapi.createAndAddServiceToInterface(h_id,i_id,port,protocol,ports=[port])
777 if type == "2":
778 v_id = guiapi.createAndAddVulnWebToService(h_id,s_id, name, desc, [], severity, "/", "/")
779 else:
780 v_id = guiapi.createAndAddVulnToService(h_id,s_id, name, desc, [],severity)
781
782 api.devlog("type:" + type)
783
784 def _isIPV4(self, ip):
785 if len(ip.split(".")) == 4:
786 return True
787 else:
788 return False
789
790 def _listNotes(self, item):
791 if item is not None and item.object is not None:
792 dialog = NotesDialog(parent=self, model_object=item.object)
793 dialog.exec_loop()
794
795 def _newNote(self, item):
796 if item is not None and item.object is not None:
797 dialog = NewNoteDialog(self, callback=self._newNoteSelectedCallback)
798 dialog.exec_loop()
799
800 def _newNoteSelectedCallback(self, name, text):
801 for i in self.items_selected:
802 if i.type == "Host":
803 api.devlog("newNotecallbackHost %s " % (i.object.name))
804 guiapi.createAndAddNoteToHost(i.object.getID(), name, text)
805 elif i.type == "Application":
806 _parent=i.object.getParent()
807 api.devlog("newNotecallbackApplication %s " % (i.object.name))
808 guiapi.createAndAddNoteToApplication(_parent.getID(), i.object.getID(), name, text)
809 elif i.type == "Interface":
810 _parent=i.object.getParent()
811 api.devlog("newNotecallbackInterface %s " % (i.object.name))
812 guiapi.createAndAddNoteToInterface(_parent.getID(), i.object.getID(), name, text)
813 elif i.type == "Service":
814 _parent=i.object.getParent().getParent()
815 api.devlog("newNotecallbackService %s " % (i.name))
816 guiapi.createAndAddNoteToService(_parent.getID(), i.object.getID(), name, text)
817
818 def _listCreds(self, item):
819 if item is not None and item.object is not None:
820 dialog = CredsDialog(parent=self, model_object=item.object)
821 dialog.exec_loop()
822
823 def _newCred(self, item):
824 api.devlog("newCred")
825 dialog = NewCredDialog(self, self._newCredSelectedCallback)
826 dialog.exec_loop()
827
828 def _importCreds(self, item):
829 filename = qt.QFileDialog.getOpenFileName(
830 CONF.getDefaultTempPath(),
831 "Csv user,pass or user:pass (*.*)",
832 None,
833 "open file dialog",
834 "Choose a password file" );
835
836 if os.path.isfile(filename):
837 with open(filename) as f:
838 data = f.read()
839 f.close()
840
841 for l in data.split():
842 api.devlog(l)
843 if re.search("^#",l):
844 api.devlog("ERROR FILE")
845 continue
846
847 d = l.split(",")
848 if len(d)<=1:
849 d = l.split(":")
850
851 api.devlog(d)
852 if len(d) <=1:
853 api.devlog("Error password line: ("+l+")" )
854 else:
855 self._newCredSelectedCallback(d[0],d[1])
856
857 def _newCredSelectedCallback(self,username,password):
858
859 for i in self.items_selected:
860 if i.type in ['Host','Service']:
861 guiapi.createAndAddCred(i.object,username,password)
862
863 def _showWorkspaceProperties(self, item):
864 if item.object is not None:
865 d = WorkspacePropertiesDialog(self, "Workspace Properties", workspace=item.object)
866 d.exec_loop()
867
868 def _modelObjectViewUpdater(self):
869 if len(self.__pendingModelObjectRedraws):
870 self.update(self.__pendingModelObjectRedraws.pop().hosts)
871 self.__pendingModelObjectRedraws[:] = []
872
873 def customEvent(self, event):
874 if event.type() == UPDATEMODEL_ID:
875 self.__pendingModelObjectRedraws.append(event)
876
877 elif event.type() == RENAMEHOSTSROOT_ID:
878 self.renameRootItem(event.name)
879
880 elif event.type() == DIFFHOSTS_ID:
881 self._diffHosts(event.old_host, event.new_host)
882
883 elif event.type() == CLEARHOSTS_ID:
884 self.clearTree()
885
886 elif event.type() == WORKSPACE_CHANGED:
887 self.workspaceChanged(event.workspace, event.workspace_type)
888
889 elif event.type() == CONFLICT_UPDATE:
890 self.updateWorkspaceName(event.nconflicts)
891
892 elif event.type() == RESOLVECONFLICTS_ID:
893 self.showResolveConflictDialog(event.conflicts)
894
895 elif event.type() == ADDHOST:
896 self._addHost(event.host)
897 self.setReindex()
898
899 elif event.type() == DELHOST:
900 self._removeHost(event.host_id)
901 self.setReindex()
902
903 elif event.type() == EDITHOST:
904 self._editHost(event.host)
905 self.setReindex()
906
907
908 def _setupContextPopups(self):
909 """
910 Configures a context popup menu for each kind of item shown in the tree.
911 This is done because different options may be needed for each item
912 """
913
914 popup = qt.QPopupMenu(self)
915
916
917
918 popup.insertSeparator()
919 popup.insertItem('Resolve Conflicts', 303)
920 popup.insertItem('Save Vulns CSV', 402)
921 popup.insertItem('Import Vulns CSV', 403)
922
923
924 popup.insertSeparator()
925 popup.insertItem('Add Host', 800)
926
927 self.contextpopups["Workspace"] = popup
928
929 self.contextpopups["Category_General"] = self.contextpopups["Workspace"]
930
931
932 popup = qt.QPopupMenu(self)
933
934
935
936
937 self.contextpopups["Category_Applications"] = popup
938
939
940 popup = qt.QPopupMenu(self)
941 popup.insertItem('Add Interfaces', 600)
942
943
944
945 self.contextpopups["Category_Interfaces"] = popup
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964 popup = qt.QPopupMenu(self)
965 popup.insertItem('Delete Host', 802)
966 popup.insertSeparator()
967 popup.insertItem('Add Interface', 600)
968 popup.insertSeparator()
969 popup.insertItem('New Vulnerability',400)
970 popup.insertItem('List Vulnerabilities',401)
971 popup.insertSeparator()
972 popup.insertItem('New note', 500)
973 popup.insertItem('Show notes', 501)
974 popup.insertSeparator()
975 popup.insertItem('New Credential', 550)
976 popup.insertItem('Show Credentials', 551)
977 popup.insertItem('Import Creds', 561)
978
979
980
981 self.contextpopups["Host"] = popup
982
983
984 popup = qt.QPopupMenu(self)
985 popup.insertItem('Delete Interface', 602)
986 popup.insertSeparator()
987 popup.insertItem('Add Service', 200)
988 popup.insertSeparator()
989 popup.insertItem('New Vulnerability',400)
990 popup.insertItem('List Vulnerabilities',401)
991 popup.insertSeparator()
992 popup.insertItem('New note', 500)
993 popup.insertItem('Show notes', 501)
994
995
996
997 self.contextpopups["Interface"] = popup
998
999
1000 popup = qt.QPopupMenu(self)
1001 popup.insertItem('Delete Service', 202)
1002 popup.insertSeparator()
1003 popup.insertItem('New Vulnerability',400)
1004 popup.insertItem('List Vulnerabilities',401)
1005 popup.insertSeparator()
1006 popup.insertItem('New note', 500)
1007 popup.insertItem('Show notes', 501)
1008 popup.insertSeparator()
1009 popup.insertItem('New Credential', 550)
1010 popup.insertItem('Show Credentials', 551)
1011 popup.insertItem('Import Creds', 561)
1012
1013
1014
1015 self.contextpopups["Service"] = popup
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025 popup = qt.QPopupMenu(self)
1026 popup.insertItem('Delete Items', 202)
1027 popup.insertSeparator()
1028 popup.insertItem('New Vulnerability Items',400)
1029 popup.insertSeparator()
1030 popup.insertItem('New note Items', 500)
1031 popup.insertSeparator()
1032 popup.insertItem('New Credential', 550)
1033 popup.insertItem('Import Creds', 561)
1034
1035 self.contextpopups["Service_Host"] = popup
1036
1037
1038
1039 popup = qt.QPopupMenu(self)
1040 popup.insertItem('Add Service', 200)
1041 popup.insertSeparator()
1042 popup.insertItem('Delete Items', 202)
1043 popup.insertSeparator()
1044 popup.insertItem('New Vulnerability Items',400)
1045 popup.insertSeparator()
1046 popup.insertItem('New note Items', 500)
1047 popup.insertSeparator()
1048 popup.insertItem('New Credential', 550)
1049 popup.insertItem('Import Creds', 561)
1050
1051
1052
1053 self.contextpopups["ServiceHost_Interface"] = popup
1054
1055
1056 popup = qt.QPopupMenu(self)
1057
1058
1059 popup.insertItem('Properties', 302)
1060 popup.insertSeparator()
1061 popup.insertItem('Add Host', 800)
1062 popup.insertSeparator()
1063 popup.insertItem('Add Service', 200)
1064 popup.insertSeparator()
1065 popup.insertItem('Delete Items', 202)
1066 popup.insertSeparator()
1067 popup.insertItem('New Vulnerability Items',400)
1068 popup.insertSeparator()
1069 popup.insertItem('New note Items', 500)
1070 popup.insertSeparator()
1071 popup.insertItem('Add Service', 200)
1072 popup.insertSeparator()
1073 popup.insertItem('New Credential', 550)
1074 popup.insertItem('Import Creds', 561)
1075
1076
1077 self.contextpopups["CategoryWorkspace_Interface"] = popup
1078
1079
1080 popup = qt.QPopupMenu(self)
1081
1082
1083 popup.insertItem('Properties', 302)
1084 popup.insertSeparator()
1085 popup.insertItem('Add Host', 800)
1086 popup.insertSeparator()
1087 popup.insertItem('Delete Items', 202)
1088 popup.insertSeparator()
1089 popup.insertItem('New Vulnerability Items',400)
1090 popup.insertSeparator()
1091 popup.insertItem('New note Items', 500)
1092 popup.insertSeparator()
1093 popup.insertItem('New Credential', 550)
1094 popup.insertItem('Import Creds', 561)
1095
1096
1097 self.contextpopups["CategoryWorkspace_ServiceHost"] = popup
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108 def _setupContextDispatchers(self):
1109 """
1110 Configures a context dispatcher for each kind of item shown in the tree.
1111 This is done because different options may be needed for each item
1112 """
1113
1114 self.contextdispatchers[100] = self._newCategory
1115 self.contextdispatchers[102] = self._delCategorymenu
1116
1117 self.contextdispatchers[200] = self._newService
1118 self.contextdispatchers[202] = self._delService
1119
1120
1121
1122 self.contextdispatchers[302] = self._showWorkspaceProperties
1123 self.contextdispatchers[303] = self._resolveConflicts
1124
1125 self.contextdispatchers[400] = self._newVuln
1126 self.contextdispatchers[401] = self._listVulns
1127 self.contextdispatchers[402] = self._listVulnsCvs
1128 self.contextdispatchers[403] = self._importVulnsCvs
1129
1130 self.contextdispatchers[500] = self._newNote
1131 self.contextdispatchers[501] = self._listNotes
1132
1133 self.contextdispatchers[550] = self._newCred
1134 self.contextdispatchers[551] = self._listCreds
1135 self.contextdispatchers[561] = self._importCreds
1136
1137 self.contextdispatchers[600] = self._newInterface
1138 self.contextdispatchers[602] = self._delInterface
1139
1140 self.contextdispatchers[800] = self._newHost
1141 self.contextdispatchers[802] = self._delHost
1142
1143
1144 def renameRootItem(self, new_name):
1145 self.rootitem.setText(0, new_name)
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 import logging
7 import threading
8 import re
9
10 from gui.customevents import LogCustomEvent
11 import model.guiapi
12 import qt
13
14
15 class GUIHandler(logging.Handler):
16 def __init__(self):
17 logging.Handler.__init__(self)
18 self._widgets = []
19 self._widgets_lock = threading.RLock()
20 formatter = logging.Formatter(
21 '%(levelname)s - %(asctime)s - %(name)s - %(message)s')
22 self.setFormatter(formatter)
23
24 def registerGUIOutput(self, widget):
25 self._widgets_lock.acquire()
26 self._widgets.append(widget)
27 self._widgets_lock.release()
28
29 def clearWidgets(self):
30 self._widgets_lock.acquire()
31 self._widgets = []
32 self._widgets_lock.release()
33
34 def emit(self, record):
35 try:
36 msg = self.format(record)
37 self._widgets_lock.acquire()
38 for widget in self._widgets:
39 event = LogCustomEvent(msg)
40 model.guiapi.postCustomEvent(event, widget)
41 self._widgets_lock.release()
42 except:
43 self.handleError(record)
44
45
46 class LogConsole(qt.QVBox):
47 """
48 widget component used to display a log or any other content in
49 a small console window
50 """
51 tag_regex = re.compile(r"^([a-zA-Z]*)\ \-.*", re.DOTALL)
52 tag_replace_regex = re.compile(r"^(([a-zA-Z]*)\ \-)")
53 tag_colors = {
54 "NOTIFICATION": "#1400F2",
55 "INFO": "#000000",
56 "WARNING": "#F5760F",
57 "ERROR": "#FC0000",
58 "CRITICAL": "#FC0000",
59 "DEBUG": "#0AC400",
60 }
61
62 def __init__(self, parent, caption=""):
63 qt.QVBox.__init__(self, parent)
64 self.setName(caption)
65 self._text_edit = qt.QTextEdit(self, caption)
66 self._text_edit.setTextFormat(qt.Qt.LogText)
67
68 def customEvent(self, event):
69 self.update(event)
70
71 def update(self, event):
72 if event.type() == 3131:
73 self.appendText(event.text)
74
75 def appendText(self, text):
76 """
77 appends new text to the console
78 """
79 m = self.tag_regex.match(text)
80 if m is not None:
81 tag = m.group(1).upper()
82 colored_tag = "<font color=\"%s\"><b>[%s]</b></font> -" % (
83 self.tag_colors.get(tag, "#000000"), tag)
84 text = self.tag_replace_regex.sub(colored_tag, text)
85 else:
86 text = "<font color=\"#000000\"><b>[INFO]</b></font> - %s" % text
87
88 self._text_edit.append(text)
89
90 def clear(self):
91 """
92 Clear the console
93 """
94 self._text_edit.clear()
95
96 def sizeHint(self):
97 """Returns recommended size of dialog."""
98 return qt.QSize(90, 30)
1212 from gui.qt3.hostsbrowser import HostsBrowser
1313 from gui.qt3.workspacebrowser import WorkspaceTreeWindow
1414 from gui.qt3.dialogs import *
15 from gui.qt3.logconsole import LogConsole
1516 from gui.qt3.configdialog import ConfigDialog
1617 from gui.qt3.toolbars import *
1718 from gui.qt3.customevents import *
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import qt
9 from gui.qt3.toolbars import PerspectiveToolbar
10 import model.api
11
12 class PerspectiveManager(qt.QVBox):
13
14 def __init__(self, parent, main_app):
15 qt.QVBox.__init__(self, parent)
16 self.setName("PerspectiveManager")
17 self.setSpacing(5)
18 self.setFrameStyle(qt.QFrame.PopupPanel | qt.QFrame.Plain)
19 self._main_app = main_app
20 self._active_perspective = None
21 self._default = ""
22 self._registered = {}
23 self._toolbar = PerspectiveToolbar(self, "perspective_toolbar")
24 self.setStretchFactor(self._toolbar, 0)
25 self._stack_panel = qt.QWidgetStack(self)
26 self._stack_panel.setFrameStyle(qt.QFrame.PopupPanel | qt.QFrame.Plain)
27 self.setStretchFactor(self._stack_panel, 10)
28
29 def _isValid(self, p):
30
31 return p.parent() == self
32
33 def registerPerspective(self, p, default=False):
34 if self._isValid(p):
35 self._stack_panel.addWidget(p)
36 if p.name() not in self._registered:
37 self._registered[p.name()] = p
38
39 p.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding))
40 self._toolbar.addPerspective(p.name())
41 if default:
42 self._default = p.name()
43 self.setActivePerspective(p.name())
44
45 def getActivePerspective(self):
46 return self._active_perspective
47
48 def setActivePerspective(self, name):
49 if isinstance(name, int):
50 name = self._toolbar.getSelectedValue()
51
52 model.api.devlog("setActivePerspective called - name = " + name)
53
54
55 if name in self._registered:
56 self._active_perspective = self._registered[name]
57 if name == "Workspaces":
58 self._active_perspective.loadAllWorkspaces()
59 self._stack_panel.raiseWidget(self._active_perspective)
60
61 def showDefaultPerspective(self):
62 self.setActivePerspective(self._default)
63
64 def getToolbar(self):
65 return self._toolbar
66
67 def sizeHint(self):
68 return qt.QSize(70, 0)
69
70 def getMainApp(self):
71 return self._main_app
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import qt
9 from gui.qt3.toolbars import PerspectiveToolbar
10 import model.api
11
12 class PerspectiveManager(qt.QVBox):
13
14 def __init__(self, parent, main_app):
15 qt.QVBox.__init__(self, parent)
16 self.setName("PerspectiveManager")
17 self.setSpacing(5)
18 self.setFrameStyle(qt.QFrame.PopupPanel | qt.QFrame.Plain)
19 self._main_app = main_app
20 self._active_perspective = None
21 self._default = ""
22 self._registered = {}
23 self._toolbar = PerspectiveToolbar(self, "perspective_toolbar")
24 self.setStretchFactor(self._toolbar, 0)
25 self._stack_panel = qt.QWidgetStack(self)
26 self._stack_panel.setFrameStyle(qt.QFrame.PopupPanel | qt.QFrame.Plain)
27 self.setStretchFactor(self._stack_panel, 10)
28
29 def _isValid(self, p):
30
31 return p.parent() == self
32
33 def registerPerspective(self, p, default=False):
34 if self._isValid(p):
35 self._stack_panel.addWidget(p)
36 if p.name() not in self._registered:
37 self._registered[p.name()] = p
38
39 p.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding))
40 self._toolbar.addPerspective(p.name())
41 if default:
42 self._default = p.name()
43 self.setActivePerspective(p.name())
44
45 def getActivePerspective(self):
46 return self._active_perspective
47
48 def setActivePerspective(self, name):
49 if isinstance(name, int):
50 name = self._toolbar.getSelectedValue()
51
52 model.api.devlog("setActivePerspective called - name = " + name)
53
54
55 if name in self._registered:
56 self._active_perspective = self._registered[name]
57 if name == "Workspaces":
58 self._active_perspective.loadAllWorkspaces()
59 self._stack_panel.raiseWidget(self._active_perspective)
60
61 def showDefaultPerspective(self):
62 self.setActivePerspective(self._default)
63
64 def getToolbar(self):
65 return self._toolbar
66
67 def sizeHint(self):
68 return qt.QSize(70, 0)
69
70 def getMainApp(self):
71 return self._main_app
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 #!/usr/bin/env python
7 import qt
8
9 class LocationToolbar(qt.QToolBar):
10
11 def __init__(self, parent, name, *args, **kwargs):
12 qt.QToolBar.__init__(self, parent, name)
13 self.setName(name)
14 hb = qt.QHBox(self)
15 self._lstr = qt.QLabel("Filter: ", hb, "Filter label")
16
17
18
19
20 self._lcombo = CustomQCombobox(True, hb, 'Filter ComboBox',parent.setFilter)
21 self._values = [""]
22 self._lcombo.insertStrList(self._values)
23 self._lcombo.setFixedSize(300, 25)
24 self._lbutton = qt.QPushButton("Set", hb, "Filter Toggle Button")
25 self._lbutton.setFixedSize(50, 25)
26 self.connect(self._lbutton, qt.SIGNAL('clicked()'), parent.setFilter)
27
28
29 def getSelectedValue(self):
30 return str(self._lcombo.currentText())
31
32 def addFilter(self, new_filter):
33 self._lcombo.insertItem(new_filter)
34
35 class CustomQCombobox(qt.QComboBox):
36 def __init__(self, rw, parent, name,callback):
37 qt.QComboBox.__init__(self, rw, parent, name)
38 self.callback=callback
39
40
41
42
43
44
45
46
47
48
49
50 def keyReleaseEvent(self, event):
51
52
53
54
55 self.callback()
56 event.ignore()
57
58 class PerspectiveToolbar(qt.QHBox):
59 def __init__(self, parent, name, *args, **kwargs):
60 qt.QHBox.__init__(self, parent, name)
61 self.setName(name)
62 self.setSpacing(0)
63 hb = qt.QHBox(self)
64 self._lstr = qt.QLabel("Perspective", hb, "perspective_label")
65 self._lcombo = qt.QComboBox(hb,'Perspective_ComboBox')
66 self.connect(self._lcombo, qt.SIGNAL('activated(int)'), parent.setActivePerspective)
67
68 def addPerspective(self, new):
69 self._lcombo.insertItem(new)
70
71 def getSelectedValue(self):
72 return str(self._lcombo.currentText())
73
74 def sizeHint(self):
75 return qt.QSize(70, 20)
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 #!/usr/bin/env python
7 import qt
8
9 class LocationToolbar(qt.QToolBar):
10
11 def __init__(self, parent, name, *args, **kwargs):
12 qt.QToolBar.__init__(self, parent, name)
13 self.setName(name)
14 hb = qt.QHBox(self)
15 self._lstr = qt.QLabel("Filter: ", hb, "Filter label")
16
17
18
19
20 self._lcombo = CustomQCombobox(True, hb, 'Filter ComboBox',parent.setFilter)
21 self._values = [""]
22 self._lcombo.insertStrList(self._values)
23 self._lcombo.setFixedSize(300, 25)
24 self._lbutton = qt.QPushButton("Set", hb, "Filter Toggle Button")
25 self._lbutton.setFixedSize(50, 25)
26 self.connect(self._lbutton, qt.SIGNAL('clicked()'), parent.setFilter)
27
28
29 def getSelectedValue(self):
30 return str(self._lcombo.currentText())
31
32 def addFilter(self, new_filter):
33 self._lcombo.insertItem(new_filter)
34
35 class CustomQCombobox(qt.QComboBox):
36 def __init__(self, rw, parent, name,callback):
37 qt.QComboBox.__init__(self, rw, parent, name)
38 self.callback=callback
39
40
41
42
43
44
45
46
47
48
49
50 def keyReleaseEvent(self, event):
51
52
53
54
55 self.callback()
56 event.ignore()
57
58 class PerspectiveToolbar(qt.QHBox):
59 def __init__(self, parent, name, *args, **kwargs):
60 qt.QHBox.__init__(self, parent, name)
61 self.setName(name)
62 self.setSpacing(0)
63 hb = qt.QHBox(self)
64 self._lstr = qt.QLabel("Perspective", hb, "perspective_label")
65 self._lcombo = qt.QComboBox(hb,'Perspective_ComboBox')
66 self.connect(self._lcombo, qt.SIGNAL('activated(int)'), parent.setActivePerspective)
67
68 def addPerspective(self, new):
69 self._lcombo.insertItem(new)
70
71 def getSelectedValue(self):
72 return str(self._lcombo.currentText())
73
74 def sizeHint(self):
75 return qt.QSize(70, 20)
0 <?xml version="1.0" encoding="UTF-8"?>
1 <glade-interface>
2 <!-- interface-requires gtk+ 2.24 -->
3 <!-- interface-naming-policy project-wide -->
4 <widget class="GtkDialog" id="exploit_dialog">
5 <property name="can_focus">False</property>
6 <property name="border_width">5</property>
7 <property name="title" translatable="yes">Faraday Report</property>
8 <property name="type_hint">dialog</property>
9 <child internal-child="vbox">
10 <widget class="GtkVBox" id="dialog-vbox1">
11 <property name="visible">True</property>
12 <property name="can_focus">False</property>
13 <property name="spacing">2</property>
14 <child internal-child="action_area">
15 <widget class="GtkHButtonBox" id="dialog-action_area1">
16 <property name="visible">True</property>
17 <property name="can_focus">False</property>
18 <property name="layout_style">end</property>
19 <child>
20 <widget class="GtkButton" id="button1">
21 <property name="label">gtk-cancel</property>
22 <property name="response_id">-6</property>
23 <property name="visible">True</property>
24 <property name="can_focus">True</property>
25 <property name="receives_default">True</property>
26 <property name="use_stock">True</property>
27 </widget>
28 <packing>
29 <property name="expand">False</property>
30 <property name="fill">False</property>
31 <property name="position">0</property>
32 </packing>
33 </child>
34 <child>
35 <widget class="GtkButton" id="button2">
36 <property name="label">gtk-ok</property>
37 <property name="response_id">-5</property>
38 <property name="visible">True</property>
39 <property name="can_focus">True</property>
40 <property name="receives_default">True</property>
41 <property name="use_stock">True</property>
42 </widget>
43 <packing>
44 <property name="expand">False</property>
45 <property name="fill">False</property>
46 <property name="position">1</property>
47 </packing>
48 </child>
49 </widget>
50 <packing>
51 <property name="expand">True</property>
52 <property name="fill">True</property>
53 <property name="position">0</property>
54 </packing>
55 </child>
56 <child>
57 <widget class="GtkTable" id="table1">
58 <property name="visible">True</property>
59 <property name="can_focus">False</property>
60 <property name="n_rows">3</property>
61 <property name="n_columns">3</property>
62 <child>
63 <placeholder/>
64 </child>
65 <child>
66 <placeholder/>
67 </child>
68 <child>
69 <widget class="GtkLabel" id="label1">
70 <property name="visible">True</property>
71 <property name="can_focus">False</property>
72 <property name="label" translatable="yes">Faraday RPC ( http//IP:PORT )</property>
73 </widget>
74 <packing>
75 <property name="top_attach">2</property>
76 <property name="bottom_attach">3</property>
77 </packing>
78 </child>
79 <child>
80 <widget class="GtkLabel" id="label2">
81 <property name="visible">True</property>
82 <property name="can_focus">False</property>
83 <property name="label" translatable="yes">Pickle file</property>
84 </widget>
85 </child>
86 <child>
87 <widget class="GtkEntry" id="faraday_rpc">
88 <property name="visible">True</property>
89 <property name="can_focus">True</property>
90 <property name="invisible_char">●</property>
91 <property name="primary_icon_activatable">False</property>
92 <property name="secondary_icon_activatable">False</property>
93 <property name="primary_icon_sensitive">True</property>
94 <property name="secondary_icon_sensitive">True</property>
95 </widget>
96 <packing>
97 <property name="left_attach">1</property>
98 <property name="right_attach">2</property>
99 <property name="top_attach">2</property>
100 <property name="bottom_attach">3</property>
101 </packing>
102 </child>
103 <child>
104 <widget class="GtkLabel" id="label3">
105 <property name="visible">True</property>
106 <property name="can_focus">False</property>
107 <property name="label" translatable="yes">Report Type</property>
108 </widget>
109 <packing>
110 <property name="top_attach">1</property>
111 <property name="bottom_attach">2</property>
112 </packing>
113 </child>
114 <child>
115 <widget class="GtkEntry" id="data_file">
116 <property name="visible">True</property>
117 <property name="can_focus">True</property>
118 <property name="invisible_char">●</property>
119 <property name="primary_icon_activatable">False</property>
120 <property name="secondary_icon_activatable">False</property>
121 <property name="primary_icon_sensitive">True</property>
122 <property name="secondary_icon_sensitive">True</property>
123 </widget>
124 <packing>
125 <property name="left_attach">1</property>
126 <property name="right_attach">2</property>
127 </packing>
128 </child>
129 <child>
130 <widget class="GtkComboBox" id="report_type">
131 <property name="visible">True</property>
132 <property name="can_focus">False</property>
133 <property name="items" translatable="yes">canvas
134 clientd</property>
135 </widget>
136 <packing>
137 <property name="left_attach">1</property>
138 <property name="right_attach">2</property>
139 <property name="top_attach">1</property>
140 <property name="bottom_attach">2</property>
141 </packing>
142 </child>
143 <child>
144 <widget class="GtkButton" id="pickle_file_button">
145 <property name="visible">True</property>
146 <property name="can_focus">True</property>
147 <property name="receives_default">True</property>
148 <property name="relief">none</property>
149 <child>
150 <widget class="GtkImage" id="image1">
151 <property name="visible">True</property>
152 <property name="can_focus">False</property>
153 <property name="stock">gtk-open</property>
154 </widget>
155 </child>
156 </widget>
157 <packing>
158 <property name="left_attach">2</property>
159 <property name="right_attach">3</property>
160 </packing>
161 </child>
162 </widget>
163 <packing>
164 <property name="expand">True</property>
165 <property name="fill">True</property>
166 <property name="position">1</property>
167 </packing>
168 </child>
169 </widget>
170 </child>
171 </widget>
172 </glade-interface>
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2
3 '''
4 Faraday Penetration Test IDE
5 Copyright (C) 2015 Infobyte LLC (http://www.infobytesec.com/)
6 See the file 'doc/LICENSE' for the license information
7
8 '''
9
10 """
11 Create a report using the libs.reports package.
12 Send the information to Faraday, using RPC API.
13
14 """
15
16 import xmlrpclib
17 import pprint
18 import sys
19 import os
20
21 if '.' not in sys.path: sys.path.append('.')
22 if 'libs' not in sys.path: sys.path.append('libs')
23 if 'exploits' not in sys.path: sys.path.append('./exploits/server/clientd')
24
25 from exploitutils import *
26
27 from libs.reports import utils
28 from libs.reports import canvas_report
29 import report as ClientdReport
30
31 from ExploitTypes.utility import Utility
32
33 NAME = 'faraday_report'
34 VERSION = '0.1'
35 DESCRIPTION = 'Creates a report from CANVAS event pickles and send the information to Faraday.'
36
37 DOCUMENTATION = {}
38 DOCUMENTATION['Repeatability'] = 'Infinite'
39 DOCUMENTATION['Usage'] = """Select the type of report to generate and supply
40 the path to a data pickle, in addition to the URL faraday RPC"""
41
42 DOCUMENTATION['Notes'] = NOTES = """This module is not backwards compatible
43 with reporting pickles created by previous versions of CANVAS.
44
45 It should also be noted that the new reporting pickle is not compatible with
46 with any of the previous CANVAS reporting modules, such as "report_timeline".
47 """
48
49 PROPERTY = {}
50 PROPERTY['TYPE'] = 'Reporting'
51 PROPERTY['SITE'] = 'Local'
52
53 DEFAULT_DATA_FILE = 'report.pkl'
54 DEFAULT_FARADAY_RPC = 'http://127.0.0.1:9876/'
55 DEFAULT_DATA_PATH = utils.get_reports_path(filename=DEFAULT_DATA_FILE)
56
57
58
59 class Host():
60 def __init__(self, ip, host_id):
61
62 self.ip = ip
63 self.host_id = host_id
64 #{IP:INTERFACE_ID}
65 self.dict_interfaces = {}
66 #{IP:{PORT:SERVICE_ID}}
67 self.dict_services = {}
68
69 def addInterface(self, ip_interface, interface_id):
70
71 self.dict_interfaces.update({ip_interface: interface_id})
72
73 def getInterfaceId(self, ip_interface):
74
75 try:
76 return self.dict_interfaces[ip_interface]
77 except:
78 return None
79
80 def addService(self, ip_interface, port, service_id):
81
82 if ip_interface in self.dict_services:
83 temp = self.dict_services[ip_interface]
84 temp.update({port: service_id})
85 self.dict_services.update({ip_interface: temp})
86 else:
87 self.dict_services.update({ ip_interface: {port:service_id} })
88
89 def getServiceId(self, ip_interface, port):
90
91 try:
92 return self.dict_services[ip_interface][port]
93 except:
94 return None
95
96
97
98 class ParsingCanvas():
99
100 def __init__(self, faraday_api, data_file):
101
102 self.faraday_api = faraday_api
103 self.data_file = data_file
104 self.data = canvas_report.Collector().collect(self.data_file)
105 self.host_list = []
106
107 def getAndCreateNewHost(self, node):
108
109 #Get OS
110 try:
111 for attack in node['attacks']:
112 if attack['node_type'] != '':
113 op_sy = attack['node_type']
114 break
115 except:
116 op_sy = 'Undefined'
117
118 #Create Host
119 host_id = self.faraday_api.createAndAddHost(
120 node['resolved_from'],
121 op_sy,
122 'Unknown',
123 'Unknown',
124 node['ip']
125 )
126 host = Host(node['ip'], host_id)
127 self.host_list.append(host)
128 return host
129
130 def getSeverity(self, cvss):
131
132 #Get severity CVSS version 3
133 values_cvss = {3.9:'Low', 6.9:'Medium', 8.9:'High', 10.0:'Critical' }
134 values = [3.9, 6.9, 8.9, 10.0]
135 #Get the score more close...
136 score = min(values, key=lambda x:abs(x-cvss))
137 return values_cvss[score]
138
139 def getAndCreateVulnerabilities(self, node, host):
140
141 #Get interface id
142 for host in self.host_list:
143 interface_id = host.getInterfaceId(node['ip'])
144 if interface_id != None:
145 break
146
147 for attack in node['attacks']:
148
149 #Create service
150 for x, y, name_exploit in self.data['_exploits']:
151
152 if name_exploit == attack['name']:
153
154 port = self.data['_exploits'][(x, y, name_exploit)]['arguments']['port']
155 ip = self.data['_exploits'][(x, y, name_exploit)]['arguments']['host']
156
157 #Check service created
158 service_id = host.getServiceId(ip, port)
159 if service_id != None:
160 break
161
162 service_id = self.faraday_api.createAndAddServiceToInterface(
163 host.host_id,
164 interface_id,
165 str(int(float(port))),
166 'tcp?',
167 int(float(port))
168 )
169
170 host.addService(ip, port, service_id)
171 break
172
173 #Create vulnerability
174 try:
175 title = attack['title']
176 description = self.data['exploits'][attack['name']]['description']
177 cve = self.data['exploits'][attack['name']]['cve']
178 severity = self.data['exploits'][attack['name']]['properties']['CVSS']
179 severity = self.getSeverity(float(severity))
180 except:
181 title = ''
182 description = ''
183 cve = []
184 severity = ''
185
186 self.faraday_api.createAndAddVulnToService(
187 host.host_id,
188 service_id,
189 title,
190 description,
191 [cve],
192 severity,
193 ''
194 )
195
196
197 def getAndCreateInterfaces(self, node, host):
198 #Get Interfaces Ipv6 or Ipv4
199 for element in self.data['_nodes']:
200
201 if element['ip'] == node['ip']:
202
203 for ip in element['ips']:
204 #Ipv6
205 if ip.find(':') > -1 and ip != '127.0.0.1':
206
207 interface_id = self.faraday_api.createAndAddInterface(
208 host.host_id,
209 ip,
210 '00:00:00:00:00:00',
211 '0.0.0.0',
212 '0.0.0.0',
213 '0.0.0.0',
214 [],
215 ip
216 )
217 host.addInterface(ip, interface_id)
218 #Ipv4
219 elif ip.find(':') <= -1 and ip != '127.0.0.1':
220
221 interface_id = self.faraday_api.createAndAddInterface(
222 host.host_id,
223 ip,
224 '00:00:00:00:00:00',
225 ip
226 )
227 host.addInterface(ip, interface_id)
228 break
229
230 def getAndCreateStatisticsLog(self, data_file, data_nodes):
231
232 #Canvas Statistics
233 ip_callback = data_nodes[0]['parent']['ip']
234 host_id = self.faraday_api.createAndAddHost(ip_callback)
235
236 text = (
237 'Exploits attempted: {0}\n'
238 'Exploits successful : {1}\n'
239 'Hosts attacked: {2}\n'
240 'Hosts compromised: {3}\n'
241 'Hosts discovered: {4}\n'
242 'Total exploits attempted: {5}\n'
243 'Total exploits successful: {6}\n'
244 ).format(
245 self.data['stats']['exploits_attempted'],
246 self.data['stats']['exploits_successful'],
247 self.data['stats']['hosts_attacked'],
248 self.data['stats']['hosts_compromised'],
249 self.data['stats']['hosts_discovered'],
250 self.data['stats']['total_exploits_attempted'],
251 self.data['stats']['total_exploits_successful']
252 )
253
254 self.faraday_api.createAndAddNoteToHost(host_id, 'Statistics canvas', text)
255
256 #Canvas log
257 log_path = os.path.dirname(data_file)
258 with open( os.path.join( log_path, 'CANVAS.log'), 'r') as file_log:
259
260 data_to_save = []
261 for line in file_log.readlines():
262
263 if line.find('canvasexploit.py') > -1 or line.find('.py] -') == -1 :
264 data_to_save.append(line.strip('\r\n'))
265
266 data_save = pprint.pformat(data_to_save)
267 self.faraday_api.createAndAddNoteToHost(host_id, 'Canvas Log', data_save )
268
269 def parsingAndSendCanvas(self):
270
271 #Iterate over hosts and create the entities.
272 hosts = self.data['hosts']
273
274 for ip in hosts:
275
276 for obj_host in self.host_list:
277
278 #Ip is a interface , not is a new host.
279 if obj_host.getInterfaceId(ip) != None:
280 self.getAndCreateVulnerabilities(
281 hosts[ip],
282 obj_host
283 )
284 break
285
286 host = self.getAndCreateNewHost(hosts[ip])
287 self.getAndCreateInterfaces(hosts[ip], host)
288 self.getAndCreateVulnerabilities(hosts[ip], host)
289
290 self.getAndCreateStatisticsLog(self.data_file, self.data['_nodes'])
291
292
293
294 class ParsingClientd(ParsingCanvas):
295
296 def __init__(self, faraday_api, data_file):
297
298 self.faraday_api = faraday_api
299 self.data_file = data_file
300 self.data = ClientdReport.Collector().collect(data_file)
301
302 def parsingAndSendClientd(self):
303 #Iterate over sessions and create the entities.
304 hosts = self.data['sessions']
305
306 for session in hosts:
307
308 #Get data
309 ip = hosts[session]['ip']
310 agent = hosts[session]['agent']
311 os = self.data['clients'][ip]['agents'][agent]['os']
312 info_host = self.data['clients'][ip]['agents'][agent]
313
314 #Create Host
315 host_id = self.faraday_api.createAndAddHost(
316 ip,
317 os,
318 'Unknown',
319 'Unknown',
320 'Unknown'
321 )
322
323 #'IE Flash' is a keyword only for Internet Explorer??'
324 try:
325 flash_player = info_host['plugins']['IE Flash']
326 except:
327 flash_player = "Unknown"
328
329 #Get information about host
330 text = (
331 'Platform: {0}\n'
332 'Language: {1}\n'
333 'Browser: {2}\n'
334 'Plugins: Flash: {3}\n'
335 'Java : {4}\n'
336 'Office: {5}\n'
337 'Agent: {6}\n'
338 'Email: {7}\n'
339 'Country: {8}\n'
340 'Cpu: {9}\n'
341 'Os: {10}\n'
342 ).format(
343 info_host['platform'],
344 info_host['language'],
345 info_host['browser'],
346 flash_player,
347 info_host['plugins']['Java'],
348 info_host['plugins']['Office'],
349 agent,
350 self.data['clients'][ip]['email'],
351 self.data['clients'][ip]['ip_country'],
352 info_host['cpu'],
353 os
354 )
355
356 #Create note with recon data.
357 self.faraday_api.createAndAddNoteToHost(host_id, 'Recon Information', text)
358
359 #If any exploit is successful
360 if (self.data['clients'][ip]['session_count'] >= 1):
361
362 for name, session_id in self.data['attacks']:
363
364 #If session Ids equals and exploits is sucessful
365 if session_id == hosts[session]['sid']:
366 if self.data['attacks'][(name, session_id)]['successful'] == True:
367
368 #Get info about vulnerability
369 name = self.data['attacks'][(name, session_id)]['exploit']['name']
370 description= self.data['attacks'][(name, session_id)]['exploit']['description']
371 ref = [self.data['attacks'][(name, session_id)]['exploit']['cve']]
372
373 #Create vulnerability
374 self.faraday_api.createAndAddVulnToHost(
375 host_id,
376 name,
377 description,
378 ref,
379 '',
380 ''
381 )
382
383
384
385 class theexploit(Utility):
386
387 def __init__(self):
388
389 Utility.__init__(self)
390 self.name = NAME
391 self.report_type = 'canvas'
392 self.data_file = DEFAULT_DATA_PATH
393 self.faraday_rpc = DEFAULT_FARADAY_RPC
394
395 def getargs(self):
396
397 self.getarg('report_type')
398 self.getarg('data_file')
399 self.getarg('faraday_rpc')
400
401 def run(self):
402
403 self.getargs()
404
405 msg = 'Sending information to Faraday ...'
406 self.log(msg)
407 self.setInfo(msg)
408
409 try:
410 faraday_api = xmlrpclib.ServerProxy(self.faraday_rpc)
411 except Exception as e:
412
413 self.log('Faraday RPC API Exception: {0}'.format(e.message))
414 self.setInfo('Faraday RPC API Exception: {0}'.format(e.message))
415
416 if self.report_type == 'canvas':
417 parser = ParsingCanvas(faraday_api, self.data_file)
418 parser.parsingAndSendCanvas()
419 else:
420 parser = ParsingClientd(faraday_api, self.data_file)
421 parser.parsingAndSendClientd()
422
423 #Finished.
424 msg = 'Done. Information sent to faraday.'
425 self.log(msg)
426 self.setInfo(msg)
427 return 1
428
429 def select_path(b, gtk, dialog, action, widget):
430
431 dialog = gtk.FileChooserDialog('Select filename...', dialog, action,
432 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK))
433 try:
434 dialog.set_filename(widget.get_text())
435
436 if dialog.run() == gtk.RESPONSE_OK:
437 fname = dialog.get_filename()
438 widget.set_text(fname)
439 finally:
440 dialog.destroy()
441
442 def dialog_update(gtk, wtree):
443
444 dialog = wtree.get_widget('exploit_dialog')
445 signal_ids = []
446
447 widget = wtree.get_widget('report_type')
448 widget.set_active(0)
449
450 widget = wtree.get_widget('data_file')
451 widget.set_text(DEFAULT_DATA_PATH)
452
453 button = wtree.get_widget('pickle_file_button')
454 sig = button.connect('clicked', select_path, gtk, dialog,
455 gtk.FILE_CHOOSER_ACTION_OPEN, widget)
456 signal_ids.append((button, sig))
457
458 widget = wtree.get_widget('faraday_rpc')
459 widget.set_text(DEFAULT_FARADAY_RPC)
460
461 def disconnect(w):
462
463 for w, sig in signal_ids:
464 w.disconnect(sig)
465 sig = dialog.connect('hide', disconnect)
466 signal_ids.append((dialog, sig))
467
468 if __name__ == '__main__':
469
470 app = theexploit()
471 ret = standard_callback_commandline(app)
4646 try:
4747 self.syncReports()
4848 except Exception:
49 model.api.devlog("An exception was captured while saving reports\n%s" % traceback.format_exc())
49 model.api.log("An exception was captured while saving reports\n%s" % traceback.format_exc())
5050 finally:
5151 tmp_timer = 0
5252
7979 if root == self._report_path:
8080 for name in files:
8181 filename = os.path.join(root, name)
82 model.api.devlog( "Report file is %s" % filename)
83
82 model.api.log( "Report file is %s" % filename)
83
8484 parser = ReportXmlParser(filename)
8585 if (parser.report_type is not None):
86 #TODO: get host and port from config
87 client = PluginControllerAPIClient("127.0.0.1", 9977)
88
89 model.api.devlog("The file is %s, %s" % (filename,parser.report_type))
86
87 host = CONF.getApiConInfoHost()
88 port_rest = int(CONF.getApiRestfulConInfoPort())
89
90 client = PluginControllerAPIClient(host, port_rest)
91
92 model.api.log("The file is %s, %s" % (filename,parser.report_type))
9093
9194 command_string = "./%s %s" % (parser.report_type.lower(), filename)
92 model.api.devlog("Executing %s" % (command_string))
95 model.api.log("Executing %s" % (command_string))
9396
9497 new_cmd, output_file = client.send_cmd(command_string)
9598 client.send_output(command_string, filename)
135138
136139 if root_tag:
137140 self.report_type = self.rType(root_tag,output)
138 model.api.devlog(self.report_type)
139141
140142 def getRootTag(self, xml_file_path):
141143 result = f = None
147149 break
148150 except SyntaxError, err:
149151 self.report_type = None
150 model.api.devlog("Not an xml file.\n %s" % (err))
152 model.api.log("Not an xml file.\n %s" % (err))
151153
152154 except IOError, err:
153155 self.report_type = None
154 model.api.devlog("Error while opening file.\n%s. %s" % (err, xml_file_path))
156 model.api.log("Error while opening file.\n%s. %s" % (err, xml_file_path))
155157 finally:
156158 f.seek(0)
157159 output=f.read()
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import os
9 import zipfile
10
11 import model.common
12 from config.configuration import getInstanceConfiguration
13 #from workspace import Workspace
14 import model.log
15 from utils.common import *
16 import shutil
17 #from plugins.api import PluginControllerAPI
18
19 CONF = getInstanceConfiguration()
20
21 # global reference only for this module to work with the model controller
22 __model_controller = None
23
24 __workspace_manager = None
25
26 _xmlrpc_api_server = None
27 _plugin_controller_api = None
28
29 #XXX: temp way to replicate info
30 _remote_servers_proxy = []
31
32 _remote_sync_server_proxy = None
33
34 # name of the currently logged user
35 __current_logged_user = ""
36
37
38 def setUpAPIs(controller, workspace_manager, hostname=None, port=None):
39 global __model_controller
40 __model_controller = controller
41 global __workspace_manager
42 __workspace_manager = workspace_manager
43 _setUpAPIServer(hostname, port)
44
45
46 def startAPIServer():
47 global _xmlrpc_api_server
48 if _xmlrpc_api_server is not None:
49 devlog("starting xmlrpc api server...")
50 #_xmlrpc_api_server.serve_forever()
51 _xmlrpc_api_server.start()
52
53
54 def stopAPIServer():
55 global _xmlrpc_api_server
56 if _xmlrpc_api_server is not None:
57 _xmlrpc_api_server.stop_server()
58 devlog("called stop on xmlrpc server")
59 _xmlrpc_api_server.join()
60 devlog("xmlrpc thread joined")
61
62
63 def _setUpAPIServer(hostname=None, port=None):
64 global _xmlrpc_api_server
65 global api_conn_info
66 if _xmlrpc_api_server is None:
67 #TODO: some way to get defaults.. from config?
68 if str(hostname) == "None":
69 hostname = "localhost"
70 if str(port) == "None":
71 port = 9876
72
73 if CONF.getApiConInfo() is None:
74 CONF.setApiConInfo(hostname, port)
75 devlog("starting XMLRPCServer with api_conn_info = %s" % str(CONF.getApiConInfo()))
76 try:
77 _xmlrpc_api_server = model.common.XMLRPCServer(CONF.getApiConInfo())
78 # Registers the XML-RPC introspection functions system.listMethods, system.methodHelp and system.methodSignature.
79 _xmlrpc_api_server.register_introspection_functions()
80
81 # register a function to nicely stop server
82 _xmlrpc_api_server.register_function(_xmlrpc_api_server.stop_server)
83
84 # register all the api functions to be exposed by the server
85 _xmlrpc_api_server.register_function(createAndAddHost)
86 _xmlrpc_api_server.register_function(createAndAddInterface)
87 _xmlrpc_api_server.register_function(createAndAddServiceToApplication)
88 _xmlrpc_api_server.register_function(createAndAddServiceToInterface)
89 _xmlrpc_api_server.register_function(createAndAddApplication)
90 _xmlrpc_api_server.register_function(createAndAddNoteToService)
91 _xmlrpc_api_server.register_function(createAndAddNoteToHost)
92 _xmlrpc_api_server.register_function(createAndAddNoteToNote)
93 _xmlrpc_api_server.register_function(createAndAddVulnWebToService)
94 _xmlrpc_api_server.register_function(createAndAddVulnToHost)
95 _xmlrpc_api_server.register_function(addHost)
96 _xmlrpc_api_server.register_function(addInterface)
97 _xmlrpc_api_server.register_function(addServiceToApplication)
98 _xmlrpc_api_server.register_function(addServiceToInterface)
99 _xmlrpc_api_server.register_function(addApplication)
100 _xmlrpc_api_server.register_function(newHost)
101 _xmlrpc_api_server.register_function(newInterface)
102 _xmlrpc_api_server.register_function(newService)
103 _xmlrpc_api_server.register_function(newApplication)
104 _xmlrpc_api_server.register_function(devlog)
105
106 #TODO: check if all necessary APIs are registered here!!
107
108 devlog("XMLRPC API server configured...")
109 except Exception, e:
110 msg = "There was an error creating the XMLRPC API Server:\n%s" % str(e)
111 log(msg)
112 devlog("[ERROR] - %s" % msg)
113
114
115 #-------------------------------------------------------------------------------
116 # APIs to create and add elements to model
117 #-------------------------------------------------------------------------------
118
119 #TODO: create a decorator to find the caller of an api to try to determine which
120 # plugin created the object
121
122
123 def createAndAddHost(name, os = "Unknown", category=None, update = False, old_hostname = None ):
124 host = newHost(name, os)
125 if addHost(host, category, update, old_hostname):
126 return host.getID()
127 return None
128
129 def createAndAddInterface(host_id, name = "", mac = "00:00:00:00:00:00",
130 ipv4_address = "0.0.0.0", ipv4_mask = "0.0.0.0",
131 ipv4_gateway = "0.0.0.0", ipv4_dns = [],
132 ipv6_address = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_prefix = "00",
133 ipv6_gateway = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_dns = [],
134 network_segment = "", hostname_resolution = []):
135 """
136 Creates a new interface object with the parameters provided and adds it to
137 the host selected.
138 If interface is successfuly created and the host exists, it returns the inteface id
139 It returns None otherwise
140 """
141 interface = newInterface(name, mac, ipv4_address, ipv4_mask, ipv4_gateway,
142 ipv4_dns,ipv6_address,ipv6_prefix,ipv6_gateway,ipv6_dns,
143 network_segment, hostname_resolution, parent_id=host_id)
144 if addInterface(host_id, interface):
145 return interface.getID()
146 return None
147
148 def createAndAddApplication(host_id, name, status = "running", version = "unknown"):
149 application = newApplication(name, status, version)
150 if addApplication(host_id, application):
151 return application.getID()
152 return None
153
154 def createAndAddServiceToApplication(host_id, application_id, name, protocol = "tcp?",
155 ports = [], status = "running", version = "unknown", description = ""):
156 service = newService(name, protocol, ports, status, version, description)
157 if addServiceToApplication(host_id, application_id, service):
158 return service.getID()
159 return None
160
161 def createAndAddServiceToInterface(host_id, interface_id, name, protocol = "tcp?",
162 ports = [], status = "running", version = "unknown", description = ""):
163 service = newService(name, protocol, ports, status, version, description, parent_id=interface_id)
164 if addServiceToInterface(host_id, interface_id, service):
165 return service.getID()
166 return None
167
168 # Vulnerability
169
170 def createAndAddVulnToHost(host_id, name, desc, ref, severity, resolution):
171 vuln = newVuln(name, desc, ref, severity, resolution, parent_id=host_id)
172 if addVulnToHost(host_id, vuln):
173 return vuln.getID()
174 return None
175
176 def createAndAddVulnToInterface(host_id, interface_id, name, desc, ref, severity, resolution):
177 vuln = newVuln(name, desc, ref, severity, resolution, parent_id=interface_id)
178 if addVulnToInterface(host_id, interface_id, vuln):
179 return vuln.getID()
180 return None
181
182 def createAndAddVulnToApplication(host_id, application_id, name, desc, ref, severity, resolution):
183 vuln = newVuln(name, desc, ref, severity, resolution)
184 if addVulnToApplication(host_id, application_id, vuln):
185 return vuln.getID()
186 return None
187
188 def createAndAddVulnToService(host_id, service_id, name, desc, ref, severity, resolution):
189 #we should give the interface_id or de application_id too? I think we should...
190 vuln = newVuln(name, desc, ref, severity, resolution, parent_id=service_id)
191 if addVulnToService(host_id, service_id, vuln):
192 return vuln.getID()
193 return None
194
195 #WebVuln
196
197 def createAndAddVulnWebToService(host_id, service_id, name, desc, ref, severity, resolution, website, path, request, response,
198 method,pname, params,query,category):
199 #we should give the interface_id or de application_id too? I think we should...
200 vuln = newVulnWeb(name, desc, ref, severity, resolution, website, path, request, response,
201 method,pname, params, query, category, parent_id=service_id)
202 if addVulnWebToService(host_id, service_id, vuln):
203 return vuln.getID()
204 return None
205
206 # Note
207
208 def createAndAddNoteToHost(host_id, name, text):
209 note = newNote(name, text, parent_id=host_id)
210 if addNoteToHost(host_id, note):
211 return note.getID()
212 return None
213
214 def createAndAddNoteToInterface(host_id, interface_id, name, text):
215 note = newNote(name, text, parent_id=interface_id)
216 if addNoteToInterface(host_id, interface_id, note):
217 return note.getID()
218 return None
219
220 def createAndAddNoteToApplication(host_id, application_id, name, text):
221 note = newNote(text)
222 if addNoteToApplication(host_id, application_id, note):
223 return note.getID()
224 return None
225
226 def createAndAddNoteToService(host_id, service_id, name, text):
227 note = newNote(name, text, parent_id=service_id)
228 if addNoteToService(host_id, service_id, note):
229 return note.getID()
230 return None
231
232 def createAndAddNoteToNote(host_id, service_id, note_id, name, text):
233 note = newNote(name, text, parent_id=note_id)
234 if addNoteToNote(host_id, service_id, note_id, note):
235 return note.getID()
236 return None
237
238 def createAndAddCredToService(host_id, service_id, username, password):
239 cred = newCred(username, password, parent_id=service_id)
240 if addCredToService(host_id, service_id, cred):
241 return cred.getID()
242 return None
243
244 #-------------------------------------------------------------------------------
245 # APIs to add already created objets to the model
246 #-------------------------------------------------------------------------------
247
248 #TODO: add class check to object passed to be sure we are adding the right thing to the model
249
250 def addHost(host, category=None, update = False, old_hostname = None):
251 if host is not None:
252 __model_controller.addHostASYNC(host, category, update, old_hostname)
253 return True
254 return False
255
256 def addInterface(host_id, interface):
257 if interface is not None:
258 __model_controller.addInterfaceASYNC(host_id, interface)
259 return True
260 return False
261
262 def addApplication(host_id, application):
263 if application is not None:
264 __model_controller.addApplicationASYNC(host_id, application)
265 return True
266 return False
267
268 def addServiceToApplication(host_id, application_id, service):
269 if service is not None:
270 __model_controller.addServiceToApplicationASYNC(host_id, application_id, service)
271 return True
272 return False
273
274 def addServiceToInterface(host_id, interface_id, service):
275 if service is not None:
276 __model_controller.addServiceToInterfaceASYNC(host_id, interface_id, service)
277 return True
278 return False
279
280 # Vulnerability
281
282 def addVulnToHost(host_id, vuln):
283 if vuln is not None:
284 __model_controller.addVulnToHostASYNC(host_id, vuln)
285 return True
286 return False
287
288 def addVulnToInterface(host_id, interface_id, vuln):
289 if vuln is not None:
290 __model_controller.addVulnToInterfaceASYNC(host_id, interface_id, vuln)
291 return True
292 return False
293
294 def addVulnToApplication(host_id, application_id, vuln):
295 if vuln is not None:
296 __model_controller.addVulnToApplicationASYNC(host_id, application_id, vuln)
297 return True
298 return False
299
300 def addVulnToService(host_id, service_id, vuln):
301 if vuln is not None:
302 __model_controller.addVulnToServiceASYNC(host_id, service_id, vuln)
303 return True
304 return False
305
306 #VulnWeb
307 def addVulnWebToService(host_id, service_id, vuln):
308 if vuln is not None:
309 __model_controller.addVulnWebToServiceASYNC(host_id, service_id, vuln)
310 return True
311 return False
312
313
314
315 # Notes
316
317
318
319
320 def addNoteToHost(host_id, note):
321 if note is not None:
322 __model_controller.addNoteToHostASYNC(host_id, note)
323 return True
324 return False
325
326 def addNoteToInterface(host_id, interface_id, note):
327 if note is not None:
328 __model_controller.addNoteToInterfaceASYNC(host_id, interface_id, note)
329 return True
330 return False
331
332 def addNoteToApplication(host_id, application_id, note):
333 if note is not None:
334 __model_controller.addNoteToApplicationASYNC(host_id, application_id, note)
335 return True
336 return False
337
338 def addNoteToService(host_id, service_id, note):
339 if note is not None:
340 __model_controller.addNoteToServiceASYNC(host_id, service_id, note)
341 return True
342 return False
343
344 def addNoteToNote(host_id, service_id, note_id, note):
345 if note is not None:
346 __model_controller.addNoteToNoteASYNC(host_id, service_id, note_id, note)
347 return True
348 return False
349
350 def addCredToService(host_id, service_id, cred):
351 if cred is not None:
352 __model_controller.addCredToServiceASYNC(host_id, service_id, cred)
353 return True
354 return False
355
356 #-------------------------------------------------------------------------------
357 # APIs to delete elements to model
358 #-------------------------------------------------------------------------------
359 #TODO: delete funcitons are still missing
360 def delHost(hostname):
361 __model_controller.delHostASYNC(hostname)
362 return True
363
364 def delApplication(hostname,appname):
365 __model_controller.delApplicationASYNC(hostname,appname)
366 return True
367
368 def delInterface(hostname,intname):
369 __model_controller.delInterfaceASYNC(hostname,intname)
370 return True
371
372 def delServiceFromHost(hostname, service):
373 __model_controller.delServiceFromHostASYNC(hostname, service)
374 return True
375
376 def delServiceFromInterface(hostname, intname, service, remote = True):
377 __model_controller.delServiceFromInterfaceASYNC(hostname,intname,service)
378 return True
379
380 def delServiceFromApplication(hostname, appname, service):
381 __model_controller.delServiceFromApplicationASYNC(hostname,appname,service)
382 return True
383
384 # Vulnerability
385 #-------------------------------------------------------------------------------
386 def delVulnFromApplication(vuln, hostname, appname):
387 __model_controller.delVulnFromApplicationASYNC(hostname, appname, vuln)
388 return True
389 #-------------------------------------------------------------------------------
390 def delVulnFromInterface(vuln, hostname, intname):
391 __model_controller.delVulnFromInterfaceASYNC(hostname,intname, vuln)
392 return True
393 #-------------------------------------------------------------------------------
394 def delVulnFromHost(vuln, hostname):
395 __model_controller.delVulnFromHostASYNC(hostname,vuln)
396 return True
397
398 #-------------------------------------------------------------------------------
399 def delVulnFromService(vuln, hostname, srvname):
400 __model_controller.delVulnFromServiceASYNC(hostname,srvname, vuln)
401 return True
402
403 # Notes
404 #-------------------------------------------------------------------------------
405 def delNoteFromApplication(note, hostname, appname):
406 __model_controller.delNoteFromApplicationASYNC(hostname, appname, note)
407 return True
408 #-------------------------------------------------------------------------------
409 def delNoteFromInterface(note, hostname, intname):
410 __model_controller.delNoteFromInterfaceASYNC(hostname,intname, note)
411 return True
412 #-------------------------------------------------------------------------------
413 def delNoteFromHost(note, hostname):
414 __model_controller.delNoteFromHostASYNC(hostname, note)
415 return True
416
417 #-------------------------------------------------------------------------------
418 def delNoteFromService(note, hostname, srvname):
419 __model_controller.delNoteFromServiceASYNC(hostname,srvname, note)
420 return True
421
422 #-------------------------------------------------------------------------------
423 def delCredFromService(cred, hostname, srvname):
424 __model_controller.delCredFromServiceASYNC(hostname,srvname, cred)
425 return True
426
427 #-------------------------------------------------------------------------------
428 # CREATION APIS
429 #-------------------------------------------------------------------------------
430
431
432 def newHost(name, os = "Unknown"):
433 """
434 It creates and returns a Host object.
435 The object created is not added to the model.
436 """
437 return __model_controller.newHost(name, os)
438
439
440 def newInterface(name = "", mac = "00:00:00:00:00:00",
441 ipv4_address = "0.0.0.0", ipv4_mask = "0.0.0.0",
442 ipv4_gateway = "0.0.0.0", ipv4_dns = [],
443 ipv6_address = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_prefix = "00",
444 ipv6_gateway = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_dns = [],
445 network_segment = "", hostname_resolution = [], parent_id=None):
446 """
447 It creates and returns an Interface object.
448 The created object is not added to the model.
449 """
450 return __model_controller.newInterface(
451 name, mac, ipv4_address, ipv4_mask, ipv4_gateway, ipv4_dns,
452 ipv6_address, ipv6_prefix, ipv6_gateway, ipv6_dns, network_segment,
453 hostname_resolution, parent_id)
454
455 def newService(name, protocol = "tcp?", ports = [], status = "running",
456 version = "unknown", description = "", parent_id=None):
457 """
458 It creates and returns a Service object.
459 The created object is not added to the model.
460 """
461 return __model_controller.newService(
462 name, protocol, ports, status, version, description, parent_id)
463
464
465 def newVuln(name, desc="", ref = None, severity="", resolution="", parent_id=None):
466 """
467 It creates and returns a Vulnerability object.
468 The created object is not added to the model.
469 """
470 return __model_controller.newVuln(
471 name, desc, ref, severity, resolution, parent_id)
472
473
474 def newVulnWeb(name, desc="", ref = None, severity="", resolution="", website="", path="", request="", response="",
475 method="",pname="", params="",query="",category="", parent_id=None):
476 """
477 It creates and returns a Vulnerability object.
478 The created object is not added to the model.
479 """
480 return __model_controller.newVulnWeb(
481 name, desc, ref, severity, resolution, website, path, request, response,
482 method, pname, params, query, category, parent_id)
483
484
485 def newNote(name, text, parent_id=None):
486 """
487 It creates and returns a Note object.
488 The created object is not added to the model.
489 """
490 return __model_controller.newNote(name, text, parent_id)
491
492
493 def newCred(username, password, parent_id=None):
494 """
495 It creates and returns a Cred object.
496 The created object is not added to the model.
497 """
498 return __model_controller.newCred(username, password, parent_id)
499
500
501 #-------------------------------------------------------------------------------
502 def newApplication(name, status = "running", version = "unknown"):
503 """
504 It creates and returns an Application object.
505 The created object is not added to the model.
506 """
507 return model.common.factory.createModelObject("HostApplication",name,
508 status = status,
509 version = version)
510
511 #-------------------------------------------------------------------------------
512
513 #getConflicts: get the current conflicts
514 def getConflicts():
515 return __model_controller.getConflicts()
516
517 #-------------------------------------------------------------------------------
518
519 #exportWorskpace
520
521 def exportWorskpace(workspace_path, export_path):
522 """
523 This api will create a zip file for the persistence directory
524 """
525 zip = zipfile.ZipFile(export_path, 'w', compression=zipfile.ZIP_DEFLATED)
526 root_len = len(os.path.abspath(workspace_path))
527 for root, dirs, files in os.walk(workspace_path):
528 if ".svn" not in root:
529 archive_root = os.path.abspath(root)[root_len:]
530 if files is not ".svn":
531 for f in files:
532 fullpath = os.path.join(root, f)
533 archive_name = os.path.join(archive_root, f)
534 # print f
535 zip.write(fullpath, archive_name, zipfile.ZIP_DEFLATED)
536 zip.close()
537
538
539 def importWorskpace(workspace_path, file_path):
540 """
541 This api will import a zip file of the persistence directory.
542 WARNING: this will overwrite any existing files!
543 """
544
545 archive = zipfile.ZipFile(str(file_path), "r", zipfile.ZIP_DEFLATED)
546 names = archive.namelist()
547
548 for name in names:
549 filename = os.path.join(workspace_path, name)
550 if not os.path.exists(os.path.dirname(filename)):
551 os.mkdir(os.path.dirname(filename))
552 # create the output file. This will overwrite any existing file with the same name
553 temp = open(filename, "wb")
554 data = archive.read(name) # read data from zip archive
555 temp.write(data)
556 temp.close()
557
558 archive.close()
559
560 #-------------------------------------------------------------------------------
561 # EVIDENCE
562 #-------------------------------------------------------------------------------
563 #TODO: refactor!! acomodar estos metodos para que no accedan a cosas directas del model_controller
564 def addEvidence(file_path):
565 """
566 Copy evidence file to the repository
567 """
568 filename=os.path.basename(file_path)
569 ###: Ver de sacar ese nombre evidences del config
570
571 dpath="%s/evidences/" % (__model_controller._persistence_dir)
572 dpathfilename="%s%s" % (dpath,filename)
573
574 #devlog("[addEvidence] File added ("+file_path+") destination path ("+dpathfilename+")")
575
576 if os.path.isfile(dpathfilename):
577 devlog("[addEvidence] - File evidence (" + dpathfilename +") exists abort adding")
578 else:
579 if not os.path.isdir(dpath):
580 os.mkdir(dpath)
581
582 shutil.copyfile(file_path,dpathfilename)
583 if os.path.isfile(dpathfilename):
584 #XXX: la idea es no acceder directamente a cosas privadas del model controller como esto de _check_evidences
585 __model_controller._check_evidences.append(dpathfilename)
586 return dpathfilename
587
588 return False
589
590 def checkEvidence(file_path):
591 """
592 Copy evidence file to the repository
593 """
594 if not os.path.isfile(file_path):
595 devlog("[addEvidence] - File evidence (" + dpathfilename +") doesnt exists abort adding")
596 else:
597 __model_controller._check_evidences.append(file_path)
598 return True
599
600 return False
601
602 def cleanEvidence():
603 """
604 Copy evidence file to the repository
605 """
606 check_evidences=__model_controller._check_evidences
607 #devlog("[cleanEvidence] check_evidence values=" + str(check_evidences))
608
609 evidence_path="%s/evidences/" % (__model_controller._persistence_dir)
610 for root, dirs, files in os.walk(evidence_path):
611 for filename in files:
612 if os.path.splitext(filename)[1].lower() == ".png":
613 f=os.path.join(root, filename)
614 if f in check_evidences:
615 devlog("[cleanEvidence] - The following file is in the evidence xml" + os.path.join(root, filename))
616 else:
617 delEvidence(f)
618 #__model_controller._check_evidences=[]
619 return True
620
621 return False
622
623 def delEvidence(file_path):
624 """
625 Add file_path to the queue to be delete from the svn and filesystem
626 """
627 if os.path.isfile(file_path):
628 devlog("[addEvidence] - Adding file (" + file_path +") to the delete queue")
629 __model_controller._deleted_evidences.append(file_path)
630 return True
631 else:
632 devlog("[addEvidence] - File evidence (" + file_path +") doesnt exist abort deleting")
633
634 return False
635
636 #-------------------------------------------------------------------------------
637 # MISC APIS
638 #-------------------------------------------------------------------------------
639 def log(msg ,level = "INFO"):
640 """
641 This api will log the text in the GUI console without the level
642 it will also log to a file with the corresponding level
643 if logger was configured that way
644 """
645 model.log.getLogger().log(msg,level)
646
647 def devlog(msg):
648 """
649 If DEBUG is set it will print information directly to stdout
650 """
651 import logging
652 if CONF.getDebugStatus():
653 logging.getLogger('faraday.model.api').debug(msg)
654 model.log.getLogger().log(msg, "DEBUG")
655
656 def showDialog(msg, level="Information"):
657 return model.log.getNotifier().showDialog(msg, level)
658
659 def showPopup(msg, level="Information"):
660 return model.log.getNotifier().showPopup(msg, level)
661
662 #-------------------------------------------------------------------------------
663 def getLoggedUser():
664 """
665 Returns the currently logged username
666 """
667 global __current_logged_user
668 return __current_logged_user
669 #-------------------------------------------------------------------------------
670
671 #TODO: implement!!!!!
672 def getLocalDefaultGateway():
673 return gateway()
674
675
676 def getActiveWorkspace():
677 return __workspace_manager.getActiveWorkspace()
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import os
9 import zipfile
10 import logging
11
12 import model.common
13 from config.configuration import getInstanceConfiguration
14 #from workspace import Workspace
15 import model.log
16 from utils.logs import getLogger
17 from utils.common import *
18 import shutil
19 #from plugins.api import PluginControllerAPI
20
21 CONF = getInstanceConfiguration()
22
23 # global reference only for this module to work with the model controller
24 __model_controller = None
25
26 __workspace_manager = None
27
28 _xmlrpc_api_server = None
29 _plugin_controller_api = None
30
31 #XXX: temp way to replicate info
32 _remote_servers_proxy = []
33
34 _remote_sync_server_proxy = None
35
36 # name of the currently logged user
37 __current_logged_user = ""
38
39
40 def setUpAPIs(controller, workspace_manager, hostname=None, port=None):
41 global __model_controller
42 __model_controller = controller
43 global __workspace_manager
44 __workspace_manager = workspace_manager
45 _setUpAPIServer(hostname, port)
46
47
48 def startAPIServer():
49 global _xmlrpc_api_server
50 if _xmlrpc_api_server is not None:
51 devlog("starting xmlrpc api server...")
52 #_xmlrpc_api_server.serve_forever()
53 _xmlrpc_api_server.start()
54
55
56 def stopAPIServer():
57 global _xmlrpc_api_server
58 if _xmlrpc_api_server is not None:
59 _xmlrpc_api_server.stop_server()
60 devlog("called stop on xmlrpc server")
61 _xmlrpc_api_server.join()
62 devlog("xmlrpc thread joined")
63
64
65 def _setUpAPIServer(hostname=None, port=None):
66 global _xmlrpc_api_server
67 global api_conn_info
68 if _xmlrpc_api_server is None:
69 #TODO: some way to get defaults.. from config?
70 if str(hostname) == "None":
71 hostname = "localhost"
72 if str(port) == "None":
73 port = 9876
74
75 if CONF.getApiConInfo() is None:
76 CONF.setApiConInfo(hostname, port)
77 devlog("starting XMLRPCServer with api_conn_info = %s" % str(CONF.getApiConInfo()))
78 try:
79 _xmlrpc_api_server = model.common.XMLRPCServer(CONF.getApiConInfo())
80 # Registers the XML-RPC introspection functions system.listMethods, system.methodHelp and system.methodSignature.
81 _xmlrpc_api_server.register_introspection_functions()
82
83 # register a function to nicely stop server
84 _xmlrpc_api_server.register_function(_xmlrpc_api_server.stop_server)
85
86 # register all the api functions to be exposed by the server
87 _xmlrpc_api_server.register_function(createAndAddHost)
88 _xmlrpc_api_server.register_function(createAndAddInterface)
89 _xmlrpc_api_server.register_function(createAndAddServiceToApplication)
90 _xmlrpc_api_server.register_function(createAndAddServiceToInterface)
91 _xmlrpc_api_server.register_function(createAndAddApplication)
92 _xmlrpc_api_server.register_function(createAndAddNoteToService)
93 _xmlrpc_api_server.register_function(createAndAddNoteToHost)
94 _xmlrpc_api_server.register_function(createAndAddNoteToNote)
95 _xmlrpc_api_server.register_function(createAndAddVulnWebToService)
96 _xmlrpc_api_server.register_function(createAndAddVulnToService)
97 _xmlrpc_api_server.register_function(createAndAddVulnToHost)
98 _xmlrpc_api_server.register_function(addHost)
99 _xmlrpc_api_server.register_function(addInterface)
100 _xmlrpc_api_server.register_function(addServiceToApplication)
101 _xmlrpc_api_server.register_function(addServiceToInterface)
102 _xmlrpc_api_server.register_function(addApplication)
103 _xmlrpc_api_server.register_function(newHost)
104 _xmlrpc_api_server.register_function(newInterface)
105 _xmlrpc_api_server.register_function(newService)
106 _xmlrpc_api_server.register_function(newApplication)
107 _xmlrpc_api_server.register_function(devlog)
108
109 #TODO: check if all necessary APIs are registered here!!
110
111 devlog("XMLRPC API server configured...")
112 except Exception, e:
113 msg = "There was an error creating the XMLRPC API Server:\n%s" % str(e)
114 log(msg)
115 devlog("[ERROR] - %s" % msg)
116
117
118 #-------------------------------------------------------------------------------
119 # APIs to create and add elements to model
120 #-------------------------------------------------------------------------------
121
122 #TODO: create a decorator to find the caller of an api to try to determine which
123 # plugin created the object
124
125
126 def createAndAddHost(name, os = "Unknown", category=None, update = False, old_hostname = None ):
127 host = newHost(name, os)
128 if addHost(host, category, update, old_hostname):
129 return host.getID()
130 return None
131
132 def createAndAddInterface(host_id, name = "", mac = "00:00:00:00:00:00",
133 ipv4_address = "0.0.0.0", ipv4_mask = "0.0.0.0",
134 ipv4_gateway = "0.0.0.0", ipv4_dns = [],
135 ipv6_address = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_prefix = "00",
136 ipv6_gateway = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_dns = [],
137 network_segment = "", hostname_resolution = []):
138 """
139 Creates a new interface object with the parameters provided and adds it to
140 the host selected.
141 If interface is successfuly created and the host exists, it returns the inteface id
142 It returns None otherwise
143 """
144 interface = newInterface(name, mac, ipv4_address, ipv4_mask, ipv4_gateway,
145 ipv4_dns,ipv6_address,ipv6_prefix,ipv6_gateway,ipv6_dns,
146 network_segment, hostname_resolution, parent_id=host_id)
147 if addInterface(host_id, interface):
148 return interface.getID()
149 return None
150
151 def createAndAddApplication(host_id, name, status = "running", version = "unknown"):
152 application = newApplication(name, status, version)
153 if addApplication(host_id, application):
154 return application.getID()
155 return None
156
157 def createAndAddServiceToApplication(host_id, application_id, name, protocol = "tcp?",
158 ports = [], status = "running", version = "unknown", description = ""):
159 service = newService(name, protocol, ports, status, version, description)
160 if addServiceToApplication(host_id, application_id, service):
161 return service.getID()
162 return None
163
164 def createAndAddServiceToInterface(host_id, interface_id, name, protocol = "tcp?",
165 ports = [], status = "running", version = "unknown", description = ""):
166 service = newService(name, protocol, ports, status, version, description, parent_id=interface_id)
167 if addServiceToInterface(host_id, interface_id, service):
168 return service.getID()
169 return None
170
171 # Vulnerability
172
173 def createAndAddVulnToHost(host_id, name, desc, ref, severity, resolution):
174 vuln = newVuln(name, desc, ref, severity, resolution, parent_id=host_id)
175 if addVulnToHost(host_id, vuln):
176 return vuln.getID()
177 return None
178
179 def createAndAddVulnToInterface(host_id, interface_id, name, desc, ref, severity, resolution):
180 vuln = newVuln(name, desc, ref, severity, resolution, parent_id=interface_id)
181 if addVulnToInterface(host_id, interface_id, vuln):
182 return vuln.getID()
183 return None
184
185 def createAndAddVulnToApplication(host_id, application_id, name, desc, ref, severity, resolution):
186 vuln = newVuln(name, desc, ref, severity, resolution)
187 if addVulnToApplication(host_id, application_id, vuln):
188 return vuln.getID()
189 return None
190
191 def createAndAddVulnToService(host_id, service_id, name, desc, ref, severity, resolution):
192 #we should give the interface_id or de application_id too? I think we should...
193 vuln = newVuln(name, desc, ref, severity, resolution, parent_id=service_id)
194 if addVulnToService(host_id, service_id, vuln):
195 return vuln.getID()
196 return None
197
198 #WebVuln
199
200 def createAndAddVulnWebToService(host_id, service_id, name, desc, ref, severity, resolution, website, path, request, response,
201 method,pname, params,query,category):
202 #we should give the interface_id or de application_id too? I think we should...
203 vuln = newVulnWeb(name, desc, ref, severity, resolution, website, path, request, response,
204 method,pname, params, query, category, parent_id=service_id)
205 if addVulnWebToService(host_id, service_id, vuln):
206 return vuln.getID()
207 return None
208
209 # Note
210
211 def createAndAddNoteToHost(host_id, name, text):
212 note = newNote(name, text, parent_id=host_id)
213 if addNoteToHost(host_id, note):
214 return note.getID()
215 return None
216
217 def createAndAddNoteToInterface(host_id, interface_id, name, text):
218 note = newNote(name, text, parent_id=interface_id)
219 if addNoteToInterface(host_id, interface_id, note):
220 return note.getID()
221 return None
222
223 def createAndAddNoteToApplication(host_id, application_id, name, text):
224 note = newNote(text)
225 if addNoteToApplication(host_id, application_id, note):
226 return note.getID()
227 return None
228
229 def createAndAddNoteToService(host_id, service_id, name, text):
230 note = newNote(name, text, parent_id=service_id)
231 if addNoteToService(host_id, service_id, note):
232 return note.getID()
233 return None
234
235 def createAndAddNoteToNote(host_id, service_id, note_id, name, text):
236 note = newNote(name, text, parent_id=note_id)
237 if addNoteToNote(host_id, service_id, note_id, note):
238 return note.getID()
239 return None
240
241 def createAndAddCredToService(host_id, service_id, username, password):
242 cred = newCred(username, password, parent_id=service_id)
243 if addCredToService(host_id, service_id, cred):
244 return cred.getID()
245 return None
246
247 #-------------------------------------------------------------------------------
248 # APIs to add already created objets to the model
249 #-------------------------------------------------------------------------------
250
251 #TODO: add class check to object passed to be sure we are adding the right thing to the model
252
253 def addHost(host, category=None, update = False, old_hostname = None):
254 if host is not None:
255 __model_controller.addHostASYNC(host, category, update, old_hostname)
256 return True
257 return False
258
259 def addInterface(host_id, interface):
260 if interface is not None:
261 __model_controller.addInterfaceASYNC(host_id, interface)
262 return True
263 return False
264
265 def addApplication(host_id, application):
266 if application is not None:
267 __model_controller.addApplicationASYNC(host_id, application)
268 return True
269 return False
270
271 def addServiceToApplication(host_id, application_id, service):
272 if service is not None:
273 __model_controller.addServiceToApplicationASYNC(host_id, application_id, service)
274 return True
275 return False
276
277 def addServiceToInterface(host_id, interface_id, service):
278 if service is not None:
279 __model_controller.addServiceToInterfaceASYNC(host_id, interface_id, service)
280 return True
281 return False
282
283 # Vulnerability
284
285 def addVulnToHost(host_id, vuln):
286 if vuln is not None:
287 __model_controller.addVulnToHostASYNC(host_id, vuln)
288 return True
289 return False
290
291 def addVulnToInterface(host_id, interface_id, vuln):
292 if vuln is not None:
293 __model_controller.addVulnToInterfaceASYNC(host_id, interface_id, vuln)
294 return True
295 return False
296
297 def addVulnToApplication(host_id, application_id, vuln):
298 if vuln is not None:
299 __model_controller.addVulnToApplicationASYNC(host_id, application_id, vuln)
300 return True
301 return False
302
303 def addVulnToService(host_id, service_id, vuln):
304 if vuln is not None:
305 __model_controller.addVulnToServiceASYNC(host_id, service_id, vuln)
306 return True
307 return False
308
309 #VulnWeb
310 def addVulnWebToService(host_id, service_id, vuln):
311 if vuln is not None:
312 __model_controller.addVulnWebToServiceASYNC(host_id, service_id, vuln)
313 return True
314 return False
315
316
317
318 # Notes
319
320
321
322
323 def addNoteToHost(host_id, note):
324 if note is not None:
325 __model_controller.addNoteToHostASYNC(host_id, note)
326 return True
327 return False
328
329 def addNoteToInterface(host_id, interface_id, note):
330 if note is not None:
331 __model_controller.addNoteToInterfaceASYNC(host_id, interface_id, note)
332 return True
333 return False
334
335 def addNoteToApplication(host_id, application_id, note):
336 if note is not None:
337 __model_controller.addNoteToApplicationASYNC(host_id, application_id, note)
338 return True
339 return False
340
341 def addNoteToService(host_id, service_id, note):
342 if note is not None:
343 __model_controller.addNoteToServiceASYNC(host_id, service_id, note)
344 return True
345 return False
346
347 def addNoteToNote(host_id, service_id, note_id, note):
348 if note is not None:
349 __model_controller.addNoteToNoteASYNC(host_id, service_id, note_id, note)
350 return True
351 return False
352
353 def addCredToService(host_id, service_id, cred):
354 if cred is not None:
355 __model_controller.addCredToServiceASYNC(host_id, service_id, cred)
356 return True
357 return False
358
359 #-------------------------------------------------------------------------------
360 # APIs to delete elements to model
361 #-------------------------------------------------------------------------------
362 #TODO: delete funcitons are still missing
363 def delHost(hostname):
364 __model_controller.delHostASYNC(hostname)
365 return True
366
367 def delApplication(hostname,appname):
368 __model_controller.delApplicationASYNC(hostname,appname)
369 return True
370
371 def delInterface(hostname,intname):
372 __model_controller.delInterfaceASYNC(hostname,intname)
373 return True
374
375 def delServiceFromHost(hostname, service):
376 __model_controller.delServiceFromHostASYNC(hostname, service)
377 return True
378
379 def delServiceFromInterface(hostname, intname, service, remote = True):
380 __model_controller.delServiceFromInterfaceASYNC(hostname,intname,service)
381 return True
382
383 def delServiceFromApplication(hostname, appname, service):
384 __model_controller.delServiceFromApplicationASYNC(hostname,appname,service)
385 return True
386
387 # Vulnerability
388 #-------------------------------------------------------------------------------
389 def delVulnFromApplication(vuln, hostname, appname):
390 __model_controller.delVulnFromApplicationASYNC(hostname, appname, vuln)
391 return True
392 #-------------------------------------------------------------------------------
393 def delVulnFromInterface(vuln, hostname, intname):
394 __model_controller.delVulnFromInterfaceASYNC(hostname,intname, vuln)
395 return True
396 #-------------------------------------------------------------------------------
397 def delVulnFromHost(vuln, hostname):
398 __model_controller.delVulnFromHostASYNC(hostname,vuln)
399 return True
400
401 #-------------------------------------------------------------------------------
402 def delVulnFromService(vuln, hostname, srvname):
403 __model_controller.delVulnFromServiceASYNC(hostname,srvname, vuln)
404 return True
405
406 # Notes
407 #-------------------------------------------------------------------------------
408 def delNoteFromApplication(note, hostname, appname):
409 __model_controller.delNoteFromApplicationASYNC(hostname, appname, note)
410 return True
411 #-------------------------------------------------------------------------------
412 def delNoteFromInterface(note, hostname, intname):
413 __model_controller.delNoteFromInterfaceASYNC(hostname,intname, note)
414 return True
415 #-------------------------------------------------------------------------------
416 def delNoteFromHost(note, hostname):
417 __model_controller.delNoteFromHostASYNC(hostname, note)
418 return True
419
420 #-------------------------------------------------------------------------------
421 def delNoteFromService(note, hostname, srvname):
422 __model_controller.delNoteFromServiceASYNC(hostname,srvname, note)
423 return True
424
425 #-------------------------------------------------------------------------------
426 def delCredFromService(cred, hostname, srvname):
427 __model_controller.delCredFromServiceASYNC(hostname,srvname, cred)
428 return True
429
430 #-------------------------------------------------------------------------------
431 # CREATION APIS
432 #-------------------------------------------------------------------------------
433
434
435 def newHost(name, os = "Unknown"):
436 """
437 It creates and returns a Host object.
438 The object created is not added to the model.
439 """
440 return __model_controller.newHost(name, os)
441
442
443 def newInterface(name = "", mac = "00:00:00:00:00:00",
444 ipv4_address = "0.0.0.0", ipv4_mask = "0.0.0.0",
445 ipv4_gateway = "0.0.0.0", ipv4_dns = [],
446 ipv6_address = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_prefix = "00",
447 ipv6_gateway = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_dns = [],
448 network_segment = "", hostname_resolution = [], parent_id=None):
449 """
450 It creates and returns an Interface object.
451 The created object is not added to the model.
452 """
453 return __model_controller.newInterface(
454 name, mac, ipv4_address, ipv4_mask, ipv4_gateway, ipv4_dns,
455 ipv6_address, ipv6_prefix, ipv6_gateway, ipv6_dns, network_segment,
456 hostname_resolution, parent_id)
457
458 def newService(name, protocol = "tcp?", ports = [], status = "running",
459 version = "unknown", description = "", parent_id=None):
460 """
461 It creates and returns a Service object.
462 The created object is not added to the model.
463 """
464 return __model_controller.newService(
465 name, protocol, ports, status, version, description, parent_id)
466
467
468 def newVuln(name, desc="", ref=None, severity="", resolution="",
469 confirmed=False, parent_id=None):
470 """
471 It creates and returns a Vulnerability object.
472 The created object is not added to the model.
473 """
474 return __model_controller.newVuln(
475 name, desc, ref, severity, resolution, confirmed, parent_id)
476
477
478 def newVulnWeb(name, desc="", ref=None, severity="", resolution="", website="",
479 path="", request="", response="", method="", pname="",
480 params="", query="", category="", confirmed=False,
481 parent_id=None):
482 """
483 It creates and returns a Vulnerability object.
484 The created object is not added to the model.
485 """
486 return __model_controller.newVulnWeb(
487 name, desc, ref, severity, resolution, website, path, request,
488 response, method, pname, params, query, category, confirmed,
489 parent_id)
490
491
492 def newNote(name, text, parent_id=None):
493 """
494 It creates and returns a Note object.
495 The created object is not added to the model.
496 """
497 return __model_controller.newNote(name, text, parent_id)
498
499
500 def newCred(username, password, parent_id=None):
501 """
502 It creates and returns a Cred object.
503 The created object is not added to the model.
504 """
505 return __model_controller.newCred(username, password, parent_id)
506
507
508 #-------------------------------------------------------------------------------
509 def newApplication(name, status = "running", version = "unknown"):
510 """
511 It creates and returns an Application object.
512 The created object is not added to the model.
513 """
514 return model.common.factory.createModelObject("HostApplication",name,
515 status = status,
516 version = version)
517
518 #-------------------------------------------------------------------------------
519
520 #getConflicts: get the current conflicts
521 def getConflicts():
522 return __model_controller.getConflicts()
523
524 #-------------------------------------------------------------------------------
525
526 #exportWorskpace
527
528 def exportWorskpace(workspace_path, export_path):
529 """
530 This api will create a zip file for the persistence directory
531 """
532 zip = zipfile.ZipFile(export_path, 'w', compression=zipfile.ZIP_DEFLATED)
533 root_len = len(os.path.abspath(workspace_path))
534 for root, dirs, files in os.walk(workspace_path):
535 if ".svn" not in root:
536 archive_root = os.path.abspath(root)[root_len:]
537 if files is not ".svn":
538 for f in files:
539 fullpath = os.path.join(root, f)
540 archive_name = os.path.join(archive_root, f)
541 # print f
542 zip.write(fullpath, archive_name, zipfile.ZIP_DEFLATED)
543 zip.close()
544
545
546 def importWorskpace(workspace_path, file_path):
547 """
548 This api will import a zip file of the persistence directory.
549 WARNING: this will overwrite any existing files!
550 """
551
552 archive = zipfile.ZipFile(str(file_path), "r", zipfile.ZIP_DEFLATED)
553 names = archive.namelist()
554
555 for name in names:
556 filename = os.path.join(workspace_path, name)
557 if not os.path.exists(os.path.dirname(filename)):
558 os.mkdir(os.path.dirname(filename))
559 # create the output file. This will overwrite any existing file with the same name
560 temp = open(filename, "wb")
561 data = archive.read(name) # read data from zip archive
562 temp.write(data)
563 temp.close()
564
565 archive.close()
566
567 #-------------------------------------------------------------------------------
568 # EVIDENCE
569 #-------------------------------------------------------------------------------
570 #TODO: refactor!! acomodar estos metodos para que no accedan a cosas directas del model_controller
571 def addEvidence(file_path):
572 """
573 Copy evidence file to the repository
574 """
575 filename=os.path.basename(file_path)
576 ###: Ver de sacar ese nombre evidences del config
577
578 dpath="%s/evidences/" % (__model_controller._persistence_dir)
579 dpathfilename="%s%s" % (dpath,filename)
580
581 #devlog("[addEvidence] File added ("+file_path+") destination path ("+dpathfilename+")")
582
583 if os.path.isfile(dpathfilename):
584 devlog("[addEvidence] - File evidence (" + dpathfilename +") exists abort adding")
585 else:
586 if not os.path.isdir(dpath):
587 os.mkdir(dpath)
588
589 shutil.copyfile(file_path,dpathfilename)
590 if os.path.isfile(dpathfilename):
591 #XXX: la idea es no acceder directamente a cosas privadas del model controller como esto de _check_evidences
592 __model_controller._check_evidences.append(dpathfilename)
593 return dpathfilename
594
595 return False
596
597 def checkEvidence(file_path):
598 """
599 Copy evidence file to the repository
600 """
601 if not os.path.isfile(file_path):
602 devlog("[addEvidence] - File evidence (" + dpathfilename +") doesnt exists abort adding")
603 else:
604 __model_controller._check_evidences.append(file_path)
605 return True
606
607 return False
608
609 def cleanEvidence():
610 """
611 Copy evidence file to the repository
612 """
613 check_evidences=__model_controller._check_evidences
614 #devlog("[cleanEvidence] check_evidence values=" + str(check_evidences))
615
616 evidence_path="%s/evidences/" % (__model_controller._persistence_dir)
617 for root, dirs, files in os.walk(evidence_path):
618 for filename in files:
619 if os.path.splitext(filename)[1].lower() == ".png":
620 f=os.path.join(root, filename)
621 if f in check_evidences:
622 devlog("[cleanEvidence] - The following file is in the evidence xml" + os.path.join(root, filename))
623 else:
624 delEvidence(f)
625 #__model_controller._check_evidences=[]
626 return True
627
628 return False
629
630 def delEvidence(file_path):
631 """
632 Add file_path to the queue to be delete from the svn and filesystem
633 """
634 if os.path.isfile(file_path):
635 devlog("[addEvidence] - Adding file (" + file_path +") to the delete queue")
636 __model_controller._deleted_evidences.append(file_path)
637 return True
638 else:
639 devlog("[addEvidence] - File evidence (" + file_path +") doesnt exist abort deleting")
640
641 return False
642
643 #-------------------------------------------------------------------------------
644 # MISC APIS
645 #-------------------------------------------------------------------------------
646 def log(msg ,level = "INFO"):
647 """
648 This api will log the text in the GUI console without the level
649 it will also log to a file with the corresponding level
650 if logger was configured that way
651 """
652 levels = {
653 "CRITICAL": logging.CRITICAL,
654 "ERROR": logging.ERROR,
655 "WARNING": logging.WARNING,
656 "INFO": logging.INFO,
657 "DEBUG": logging.DEBUG,
658 "NOTSET": logging.NOTSET
659 }
660 level = levels.get(level, logging.NOTSET)
661 getLogger().log(level, msg)
662
663 def devlog(msg):
664 """
665 If DEBUG is set it will print information directly to stdout
666 """
667 getLogger().debug(msg)
668
669 def showDialog(msg, level="Information"):
670 return model.log.getNotifier().showDialog(msg, level)
671
672 def showPopup(msg, level="Information"):
673 return model.log.getNotifier().showPopup(msg, level)
674
675 #-------------------------------------------------------------------------------
676 def getLoggedUser():
677 """
678 Returns the currently logged username
679 """
680 global __current_logged_user
681 return __current_logged_user
682 #-------------------------------------------------------------------------------
683
684 #TODO: implement!!!!!
685 def getLocalDefaultGateway():
686 return gateway()
687
688
689 def getActiveWorkspace():
690 return __workspace_manager.getActiveWorkspace()
137137 # report manager
138138
139139 last_workspace = CONF.getLastWorkspace()
140 if not self._workspace_manager.workspaceExists(last_workspace):
141 getLogger(self).info("Your last workspace ("+last_workspace+") wasn't accessible, check configuration...")
142 self._workspace_manager.openDefaultWorkspace()
143 #self._workspace_manager.createWorkspace(last_workspace, 'default workspace, probably created already in couchb')
144 else:
145 self._workspace_manager.openWorkspace(last_workspace)
140 try:
141 if not self._workspace_manager.workspaceExists(last_workspace):
142 getLogger(self).info("Your last workspace ("+str(last_workspace)+") wasn't accessible, check configuration...")
143 self._workspace_manager.openDefaultWorkspace()
144 #self._workspace_manager.createWorkspace(last_workspace, 'default workspace, probably created already in couchb')
145 else:
146 self._workspace_manager.openWorkspace(last_workspace)
147 except restkit.errors.Unauthorized:
148 print "You are trying to enter CouchDB with authentication"
149 print "Add your credentials to your user configuration file in $HOME/.faraday/config/user.xml"
150 print "For example: <couch_uri>http://john:[email protected]:5984</couch_uri>"
151 return
146152
147153 model.api.setUpAPIs(
148154 self._model_controller,
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 """
7 Contains base classes used to represent the application model
8 and some other common objects and functions used in the model
9 """
10 import sys
11 import os
12 import traceback
13 import threading
14 import SimpleXMLRPCServer
15 import xmlrpclib
16 from utils.decorators import updateLocalMetadata
17 import json
18 import model
19 from conflict import ConflictUpdate
20 from model.diff import ModelObjectDiff
21
22 try:
23 import model.api as api
24 except AttributeError:
25 import api
26 from utils.common import *
27
28 #----------- Metadata history for timeline support, prob. we should move this out model common
29
30 from time import time
31 import cPickle as pickle
32 from config.configuration import getInstanceConfiguration
33
34 class MetadataUpdateActions(object):
35 """Constants for the actions made on the update"""
36 UNDEFINED = -1
37 CREATE = 0
38 UPDATE = 1
39
40 class Metadata(object):
41 """To save information about the modification of ModelObjects.
42 All members declared public as this is only a wrapper"""
43
44 class_signature = "Metadata"
45
46 def __init__(self, user):
47 self.creator = user
48 self.owner = user
49 self.create_time = time()
50 self.update_time = time()
51 self.update_user = user
52 self.update_action = MetadataUpdateActions.CREATE
53 self.update_controller_action = self.__getUpdateAction()
54
55 def toDict(self):
56 return self.__dict__
57
58 def fromDict(self, dictt):
59 for k, v in dictt.items():
60 setattr(self, k, v)
61 return self
62
63
64 def update(self, user, action = MetadataUpdateActions.UPDATE):
65 """Update the local metadata giving a user and an action.
66 Update time gets modified to the current system time"""
67 self.update_user = user
68 self.update_time = time()
69 self.update_action = action
70
71 self.update_controller_action = self.__getUpdateAction()
72
73 # api.devlog("Updating object (%s) metadata for user: (%s), utime = (%.4f), action=(%d), controller action = (%s)"
74 # % (self, self.update_user, self.update_time, self.update_action, self.update_controller_action))
75
76 def __getUpdateAction(self):
77 """This private method grabs the stackframes in look for the controller
78 call that generated the update"""
79
80 l_strace = traceback.extract_stack(limit = 10)
81 controller_funcallnames = [ x[2] for x in l_strace if "controller" in x[0] ]
82
83 if controller_funcallnames:
84 return "ModelControler." + " ModelControler.".join(controller_funcallnames)
85 return "No model controller call"
86
87
88 class ModelObject(object):
89 """
90 This is the base class for every object we need to represent in the
91 system (like hosts, services, etc)
92 It defines some generic methods to handle internal attributes that are
93 dictionaries. It also has generic methods to deal with notes & vulns
94 since every object could have them.
95 """
96 # this static attribute used with a factory
97 class_signature = "ModelObject"
98
99 def __init__(self, parent_id=None):
100 self._name = ""
101 self._id = None
102 self._parent_id = parent_id
103 self._parent = None
104
105 self.owner = api.getLoggedUser()
106 self._metadata = Metadata(self.owner)
107
108 # indicates if object was owned somehow
109 # we could use this property to put a different color on the GUI
110 self._is_owned = False
111
112 # a custom description given by the user
113 # this can be used to explain the purpose of the object
114 self.description = ""
115
116 self.publicattrs = {'Description':'description',
117 'Name':'getName','Owned':'isOwned'
118 }
119
120 self.publicattrsrefs = {'Description': '_description',
121 'Name': '_name','Owned': '_is_owned'
122 }
123
124 self._updatePublicAttributes()
125
126 #TODO: I think notes and vulns should be a dict
127 self._notes = {}
128 self._vulns = {}
129 self._creds = {}
130 self.evidences = []
131
132 self.updates = []
133
134 def accept(self, visitor):
135 visitor.visit(self)
136
137 def defaultValues(self):
138 return [-1, 0, '', 'unknown', None, [], {}]
139
140 def __getAttribute(self, key):
141 """ Looks for the attribute beneth the public attribute dict """
142 return self.publicattrsrefs.get(key)
143
144 def propertyTieBreaker(self, key, prop1, prop2):
145 """ Breakes the conflict between two properties. If either of them
146 is a default value returns the true and only.
147 If neither returns the default value.
148 If conflicting returns a tuple with the values """
149 if prop1 in self.defaultValues(): return prop2
150 elif prop2 in self.defaultValues(): return prop1
151 elif self.tieBreakable(key): return self.tieBreak(key, prop1, prop2)
152 else: return (prop2, prop1)
153
154 def tieBreakable(self, key):
155 return False
156
157 def tieBreak(self, key, prop1, prop2):
158 return None
159
160 def addUpdate(self, newModelObject):
161 conflict = False
162 diff = ModelObjectDiff(self, newModelObject)
163 for k, v in diff.getPropertiesDiff().items():
164 attribute = self.__getAttribute(k)
165 prop_update = self.propertyTieBreaker(attribute, *v)
166 if isinstance(prop_update, tuple):
167 conflict = True
168 else:
169 setattr(self, attribute, prop_update)
170 if conflict:
171 self.updates.append(ConflictUpdate(self, newModelObject))
172 return conflict
173
174 def needs_merge(self, new_obj):
175 return ModelObjectDiff(self, new_obj).existDiff()
176
177 def getUpdates(self):
178 return self.updates
179
180 def updateResolved(self, update):
181 self.updates.remove(update)
182
183
184 # IMPORTANT: all delete methods are considered FULL delete
185 # this means it will delete the reference from host and all
186 # other objects containing them
187 def _getValueByID(self, attrName, ID):
188 """
189 attribute passed as a parameter MUST BE a dictionary indexed with a
190 string ID
191 if id is found as a part of a key it returns the object
192 it returns None otherwise
193 """
194 if ID:
195 hash_id = get_hash([ID])
196 ref = self.__getattribute__(attrName)
197 # we are assuming the value is unique inside the object ID's
198 for key in ref:
199 #XXX: this way of checking the ids doesn't allow us to use a real hash as key
200 # because we are checking if "id" is part of the key... not a good way of
201 # dealing with this...
202 if hash_id == key or ID == key:
203 return ref[key]
204 # if id (hash) was not found then we try with element names
205 for element in ref.itervalues():
206 #if id in element.name:
207 if ID == element.name:
208 return element
209 return None
210
211
212 def _addValue(self, attrName, newValue, setparent = False, update = False):
213 # attribute passed as a parameter MUST BE the name
214 # of an internal attribute which is a dictionary indexed
215 # with a string ID
216 valID = newValue.getID()
217 ref = self.__getattribute__(attrName)
218 if valID not in ref or update:
219 ref[valID] = newValue
220 if setparent:
221 newValue.setParent(self)
222 return True
223 #return not update
224 return False
225
226
227 def _updatePublicAttributes(self):
228 # can be overriden if needed
229 pass
230
231 def setID(self, ID=None):
232 if ID is None:
233 self.updateID()
234 else:
235 self._id = ID
236 return self._id
237
238 def updateID(self):
239 raise NotImplementedError("This should be overwritten")
240
241 def _prependParentId(self):
242 if self._parent_id:
243 self._id = '.'.join((self._parent_id, self.getID()))
244
245 def getID(self):
246 if self._id is None:
247 self.updateID()
248 return self._id
249
250 id = property(getID, setID)
251
252 def getMetadata(self):
253 """Returns the current metadata of the object"""
254 return self._metadata
255
256 def setMetadata(self, metadata):
257 self._metadata = metadata
258
259 def getMetadataHistory(self):
260 """Returns the current metadata of the object"""
261 return self._metadataHistory
262
263 def updateMetadata(self):
264 """ We are only saving the previous state so the newst is not available"""
265 self.getMetadata().update(self.owner)
266 # self.getMetadataHistory().pushMetadataForId(self.getID(), self.getMetadata())
267
268 def getHost(self):
269 #recursive method to recover the Host root
270 if self.class_signature == "Host":
271 return self
272 return self.getParent().getHost()
273
274 def setName(self, name):
275 self._name = name
276
277 def getName(self):
278 return self._name
279
280 name = property(getName, setName)
281
282 def setDescription(self, description):
283 self._description = description
284
285 def getDescription(self):
286 return self._description
287
288 description = property(getDescription, setDescription)
289
290 def isOwned(self):
291 return self._is_owned
292
293 def setOwned(self, owned=True):
294 self._is_owned = owned
295
296 def getOwner(self):
297 return self.owner
298
299 def setOwner(self, owner=None):
300 self.owner = owner
301
302 #@save
303 def setParent(self, parent):
304 self._parent = parent
305
306 def getParent(self):
307 return self._parent
308
309 parent = property(getParent, setParent)
310
311 #TODO: this should be removed and we could use some class
312 # based on dict to implement this
313
314
315 def _delValue(self, attrName, valID):
316 # attribute passed as a parameter MUST BE the name
317 # of an internal attribute which is a dictionary indexed
318 # with a string ID
319 api.devlog("(%s)._delValue(%s, %s)" % (self, attrName, valID))
320 ref = self.__getattribute__(attrName)
321 api.devlog("ref.keys() = %s" % ref.keys())
322 if valID in ref:
323 val = ref[valID]
324 del ref[valID]
325 val.delete()
326 return True
327
328 hash_id = get_hash([valID])
329 if hash_id in ref:
330 val = ref[hash_id]
331 del ref[hash_id]
332 val.delete()
333 return True
334
335 for element in ref.itervalues():
336 if valID == element.name:
337 val = ref[element.getID()]
338 del ref[element.getID()]
339 val.delete()
340 return True
341
342 # none of the ids were found
343 return False
344
345 def _delAllValues(self, attrName):
346 ref = self.__getattribute__(attrName)
347 try:
348 ref.clear()
349 return True
350 except Exception:
351 return False
352
353 #@delete
354 def delete(self):
355 del self
356
357 def _getAllValues(self, attrName, mode = 0):
358 """
359 attribute passed as a parameter MUST BE a dictionary indexed with a
360 string ID
361 return all values in the dictionary
362 mode = 0 returns a list of objects
363 mode = 1 returns a dictionary of objects with their id as key
364 """
365 ref = self.__getattribute__(attrName)
366 if mode:
367 return ref
368 else:
369 return sorted(ref.values())
370
371 def _getAllIDs(self, attrName):
372 ref = self.__getattribute__(attrName)
373 return ref.keys()
374
375 def _getValueCount(self, attrName):
376 ref = self.__getattribute__(attrName)
377 return len(ref)
378
379 def __repr__(self):
380 return "<ModelObject %s at 0x%x>" % (self.__class__.__name__, id(self))
381
382 def __str__(self):
383 return "<ModelObject %s ID = %s at 0x%x>" % (self.__class__.__name__, self._id, id(self))
384
385 #notes
386 @updateLocalMetadata
387 def addNote(self, newNote, update=False, setparent=True):
388 self.addChild(newNote)
389 return True
390
391 def newNote(self, name, text):
392 note = ModelObjectNote(name, text, self)
393 self.addNote(note)
394
395 @updateLocalMetadata
396 def delNote(self, noteID):
397 self.deleteChild(noteID)
398 return True
399
400 def getNotes(self):
401 return self.getChildsByType(ModelObjectNote.__name__)
402
403 def setNotes(self, notes):
404 self._addChildsDict(notes)
405
406 def getNote(self, noteID):
407 return self.findChild(noteID)
408
409 def notesCount(self):
410 return len(self._notes.values())
411
412 #Vulnerability
413 @updateLocalMetadata
414 def addVuln(self, newVuln, update=False, setparent=True):
415 self.addChild(newVuln)
416 return True
417
418 @updateLocalMetadata
419 def delVuln(self, vulnID):
420 self.deleteChild(vulnID)
421 return True
422
423 def getVulns(self):
424 return self.getChildsByType(ModelObjectVuln.__name__) + self.getChildsByType(ModelObjectVulnWeb.__name__)
425
426 def setVulns(self, vulns):
427 self._addChildsDict(vulns)
428
429 def getVuln(self, vulnID):
430 return self.findChild(vulnID)
431
432 def vulnsCount(self):
433 return len(self._vulns.values())
434
435 def vulnsToDict(self):
436 d = []
437 for vuln in self._vulns.values():
438 d.append(vuln.toDictFull())
439 return d
440
441 @updateLocalMetadata
442 def delCred(self, credID):
443 return self._delValue("_creds", credID)
444
445 def getCreds(self):
446 return self.getChildsByType(ModelObjectCred.__name__)
447
448 def setCreds(self, creds):
449 self._addChildsDict(creds)
450
451 def getCred(self, credID):
452 return self._getValueByID("_creds", credID)
453
454 def credsToDict(self):
455 d = []
456 for cred in self._creds.values():
457 d.append(cred.toDictFull())
458 return d
459
460
461 def credsCount(self):
462 return len(self._creds.values())
463
464 def __getClassname(self, val):
465 supported = factory.listModelObjectTypes()
466 return filter(lambda x: val.lower().replace('_', '')[:-1] in x.lower(), supported)[0]
467
468 def _asdict(self):
469 return self.toDictFull()
470
471 def ancestors_path(self):
472 if self.getParent() is None:
473 return str(self.getID())
474 return ".".join([self.getParent().ancestors_path()] + [str(self.getID())])
475
476 def _addChildsDict(self, dictt):
477 [self.addChild(v) for k, v in dictt.items()]
478
479
480 class ModelComposite(ModelObject):
481 """ Model Objects Composite Abstract Class """
482
483 def __init__(self, parent_id=None):
484 ModelObject.__init__(self, parent_id)
485 self.childs = {}
486
487 def addChild(self, model_object):
488 model_object.setParent(self)
489 self.childs[model_object.getID()] = model_object
490
491 def getChilds(self):
492 return self.childs
493
494 def getChildsByType(self, signature):
495 return [c for c in self.childs.values()
496 if c.__class__.__name__ == signature]
497
498 def deleteChild(self, iid):
499 del self.childs[iid]
500
501 def findChild(self, iid):
502 return self.childs.get(iid)
503
504 class ModelLeaf(ModelObject):
505 def __init__(self, parent_id=None):
506 ModelObject.__init__(self, parent_id)
507
508 def getChildsByType(self, signature):
509 return []
510
511 def getChilds(self):
512 return {}
513
514 #-------------------------------------------------------------------------------
515 #TODO: refactor this class to make it generic so this can be used also for plugins
516 # then create a subclass and inherit the generic factory
517 class ModelObjectFactory(object):
518 """
519 Factory to creat any ModelObject type
520 """
521 def __init__(self):
522 self._registered_objects = dict()
523
524 def register(self, model_object):
525 """registers a class into the factory"""
526 self._registered_objects[model_object.class_signature] = model_object
527
528 def listModelObjectClasses(self):
529 """returns a list of registered classes"""
530 return self._registered_objects.values()
531
532 def getModelObjectClass(self, name):
533 """get the class for a particular object typename"""
534 return self._registered_objects[name]
535
536 def listModelObjectTypes(self):
537 """returns an array with object typenames the factory is able to create"""
538 names = self._registered_objects.keys()
539 names.sort()
540 return names
541
542 def generateID(self, classname, parent_id=None, **objargs):
543 tmpObj = self._registered_objects[classname](**objargs)
544 if parent_id:
545 return '.'.join([parent_id, tmpObj.getID()])
546 return tmpObj.getID()
547
548 def createModelObject(self, classname, object_name=None, **objargs):
549 if classname in self._registered_objects:
550 if object_name is not None:
551 tmpObj = self._registered_objects[classname](object_name,**objargs)
552 return tmpObj
553 else:
554 raise Exception("Object name parameter missing. Cannot create object class: %s" % classname)
555 else:
556 raise Exception("Object class %s not registered in factory. Cannot create object." % classname)
557
558 #-------------------------------------------------------------------------------
559 # global reference kind of a singleton
560 factory = ModelObjectFactory()
561
562 #-------------------------------------------------------------------------------
563
564 class CustomXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
565
566 def __init__(self, *args, **kwargs):
567 SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.__init__(self, *args, **kwargs)
568
569 def handle(self):
570 try:
571 api.devlog("-" * 60)
572 api.devlog("[XMLRPCHandler] - request = %s" % str(self.request))
573 api.devlog("[XMLRPCHandler] - client_address = %s" % str(self.client_address))
574 api.devlog("[XMLRPCHandler] - server = %s" % str(self.server))
575 api.devlog("-" * 60)
576 SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.handle(self)
577 except Exception:
578 api.devlog("[XMLRPCHandler] - An error ocurred while handling a request\n%s" % traceback.format_exc())
579
580 def do_POST(self):
581 """
582 Handles the HTTP POST request.
583 Attempts to interpret all HTTP POST requests as XML-RPC calls,
584 which are forwarded to the server's _dispatch method for handling.
585
586 This is a copy of the original do_POST, but it sends information about
587 the client calling the server to the marshaled dispatch. This info
588 can be later passed to the server
589 """
590
591 # Check that the path is legal
592 if not self.is_rpc_path_valid():
593 self.report_404()
594 return
595
596 try:
597 # Get arguments by reading body of request.
598 # We read this in chunks to avoid straining
599 # socket.read(); around the 10 or 15Mb mark, some platforms
600 # begin to have problems (bug #792570).
601 max_chunk_size = 10*1024*1024
602 size_remaining = int(self.headers["content-length"])
603 L = []
604 while size_remaining:
605 chunk_size = min(size_remaining, max_chunk_size)
606 L.append(self.rfile.read(chunk_size))
607 size_remaining -= len(L[-1])
608 data = ''.join(L)
609
610 # In previous versions of SimpleXMLRPCServer, _dispatch
611 # could be overridden in this class, instead of in
612 # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
613 # check to see if a subclass implements _dispatch and dispatch
614 # using that method if present.
615 response = self.server._marshaled_dispatch(
616 data, getattr(self, '_dispatch', None)
617 )
618 except Exception, e: # This should only happen if the module is buggy
619 # internal error, report as HTTP server error
620 self.send_response(500)
621
622 # Send information about the exception if requested
623 if hasattr(self.server, '_send_traceback_header') and \
624 self.server._send_traceback_header:
625 self.send_header("X-exception", str(e))
626 self.send_header("X-traceback", traceback.format_exc())
627
628 self.end_headers()
629 else:
630 # got a valid XML RPC response
631 self.send_response(200)
632 self.send_header("Content-type", "text/xml")
633 self.send_header("Content-length", str(len(response)))
634 self.end_headers()
635 self.wfile.write(response)
636
637 # shut down the connection
638 self.wfile.flush()
639 self.connection.shutdown(1)
640 #-------------------------------------------------------------------------------
641 # custom XMLRPC server with stopping function
642 #TODO: check http://epydoc.sourceforge.net/stdlib/SimpleXMLRPCServer.SimpleXMLRPCServer-class.html
643 # see if there is a way to know the ip caller
644 # looks like the request handler can give us that info
645 # http://epydoc.sourceforge.net/stdlib/BaseHTTPServer.BaseHTTPRequestHandler-class.html#address_string
646 #
647
648 class XMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer, threading.Thread):
649 """
650 Stoppable XMLRPC Server with custom dispatch to send over complete traceback
651 in case of exception.
652 """
653 def __init__(self, *args, **kwargs):
654 threading.Thread.__init__(self)
655 SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, requestHandler = CustomXMLRPCRequestHandler, allow_none = True, *args,**kwargs)
656 self._stop = False
657 # set timeout for handle_request. If we don't the server will hang
658 self.timeout = 2
659
660 def run(self):
661 self.serve_forever()
662 api.devlog("serve_forever ended")
663 return
664
665 # overloaded method to be able to stop server
666 def serve_forever(self):
667 while not self._stop:
668 self.handle_request()
669 api.devlog("server forever stopped by flag")
670
671 def stop_server(self):
672 api.devlog("server stopping...")
673 self._stop = True
674
675 # The default dispatcher does not send across the whole stack trace.
676 # Only type and value are passed back. The client has no way of knowing
677 # the exact place where error occurred in the server (short of some
678 # other means such as server logging). This dispatcher sends the whole
679 # stack trace.
680 def _dispatch(self, method, params):
681 """Dispatches the XML-RPC method.
682
683 XML-RPC calls are forwarded to a registered function that
684 matches the called XML-RPC method name. If no such function
685 exists then the call is forwarded to the registered instance,
686 if available.
687
688 If the registered instance has a _dispatch method then that
689 method will be called with the name of the XML-RPC method and
690 its parameters as a tuple
691 e.g. instance._dispatch('add',(2,3))
692
693 If the registered instance does not have a _dispatch method
694 then the instance will be searched to find a matching method
695 and, if found, will be called.
696
697 Methods beginning with an '_' are considered private and will
698 not be called.
699 """
700
701 func = None
702 try:
703 # check to see if a matching function has been registered
704 func = self.funcs[method]
705 except KeyError:
706 if self.instance is not None:
707 # check for a _dispatch method
708 if hasattr(self.instance, '_dispatch'):
709 return self.instance._dispatch(method, params)
710 else:
711 # call instance method directly
712 try:
713 func = SimpleXMLRPCServer.resolve_dotted_attribute(
714 self.instance,
715 method,
716 self.allow_dotted_names
717 )
718 except AttributeError:
719 pass
720
721 if func is not None:
722 try:
723 # since we are using a keyword xmlrpc proxy this is sending
724 # the info comes in form of args and kwargs
725 # so params has 2 items, the first being a list or tuple
726 # and the second a dictionary
727 if len(params) == 2 and isinstance(params[1],dict) and\
728 ( isinstance(params[0],list) or isinstance(params[0],tuple) ) :
729 return func(*params[0], **params[1])
730 else:
731 # this is the default way in case a normal xmlrpclib.ServerProxy is used
732 return func(*params)
733 except Exception:
734 # extended functionality to let the client have the full traceback
735 msg = traceback.format_exc()
736 raise xmlrpclib.Fault(1, msg)
737 else:
738 raise Exception('method "%s" is not supported' % method)
739
740
741 def _marshaled_dispatch(self, data, dispatch_method = None):
742 """Dispatches an XML-RPC method from marshalled (XML) data.
743
744 XML-RPC methods are dispatched from the marshalled (XML) data
745 using the _dispatch method and the result is returned as
746 marshalled data. For backwards compatibility, a dispatch
747 function can be provided as an argument (see comment in
748 SimpleXMLRPCRequestHandler.do_POST) but overriding the
749 existing method through subclassing is the prefered means
750 of changing method dispatch behavior.
751 """
752
753 try:
754 params, method = xmlrpclib.loads(data)
755
756 # generate response
757 if dispatch_method is not None:
758 response = dispatch_method(method, params)
759 else:
760 response = self._dispatch(method, params)
761 # wrap response in a singleton tuple
762 response = (response,)
763 response = xmlrpclib.dumps(response, methodresponse=1,
764 allow_none=self.allow_none, encoding=self.encoding)
765 except Fault, fault:
766 response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
767 encoding=self.encoding)
768 except Exception:
769 # report exception back to server
770 exc_type, exc_value, exc_tb = sys.exc_info()
771 response = xmlrpclib.dumps(
772 xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
773 encoding=self.encoding, allow_none=self.allow_none,
774 )
775
776 return response
777
778 #-------------------------------------------------------------------------------
779
780 class XMLRPCKeywordProxy(object):
781 """
782 custom XMLRPC Server Proxy capable of receiving keyword arguments
783 when calling remote methods
784 """
785 def __init__(self, *args, **kwargs):
786 self._xmlrpc_server_proxy = xmlrpclib.ServerProxy(*args, **kwargs)
787 def __getattr__(self, name):
788 call_proxy = getattr(self._xmlrpc_server_proxy, name)
789 def _call(*args, **kwargs):
790 return call_proxy(args, kwargs)
791 return _call
792
793
794
795 #-------------------------------------------------------------------------------
796 class ModelObjectNote(ModelComposite):
797 """
798 Simple class to store notes about any object.
799 id will be used to number notes (based on a counter on the object being commented)
800 parent will be a reference to the object being commented.
801 To assing new text this:
802 >>> note.text = "foobar"
803 to append text + or += operators can be used (no need to use text property):
804 >>> note += " hello world!"
805 """
806 class_signature = "Note"
807
808 def __init__(self, name="", text="", parent_id=None):
809 ModelComposite.__init__(self, parent_id)
810 self.name = str(name)
811 self._text = str(text)
812
813 def updateID(self):
814 self._id = get_hash([self.name, self._text])
815 self._prependParentId()
816
817 def _setText(self, text):
818 # clear buffer then write new text
819 # self._text.seek(0)
820 # self._text.truncate()
821 # self._text.write(text)
822 self._text = str(text)
823
824 def _getText(self):
825 # return self._text.getvalue()
826 return self._text
827
828 def getText(self):
829 # return self._text.getvalue()
830 return self._text
831
832 def setText(self, text):
833 # return self._text.getvalue()
834 self._text = str(text)
835
836 text = property(_getText, _setText)
837
838 #@save
839 @updateLocalMetadata
840 def updateAttributes(self, name=None, text=None):
841 if name is not None:
842 self.setName(name)
843 if text is not None:
844 self.text = text
845
846 def __str__(self):
847 return "%s: %s" % (self.name, self.text)
848
849 def __repr__(self):
850 return "%s: %s" % (self.name, self.text)
851
852 #-------------------------------------------------------------------------------
853 class ModelObjectVuln(ModelComposite):
854 """
855 Simple class to store vulnerability about any object.
856 id will be used to number vulnerability (based on a counter on the object being commented)
857 parent will be a reference to the object being commented.
858 """
859 class_signature = "Vulnerability"
860
861 def __init__(self, name="",desc="", ref=None, severity="", resolution="", parent_id=None):
862 """
863 The parameters refs can be a single value or a list with values
864 """
865 ModelComposite.__init__(self, parent_id)
866 self.name = name
867 self._desc = desc
868 self.data = ""
869
870 self.refs = []
871
872 if isinstance(ref, list):
873 self.refs.extend(ref)
874 elif ref is not None:
875 self.refs.append(ref)
876
877 # Severity Standarization
878 self.severity = self.standarize(severity)
879 self.resolution = resolution
880
881 def standarize(self, severity):
882 # Transform all severities into lower strings
883 severity = str(severity).lower()
884 # If it has info, med, high, critical in it, standarized to it:
885
886
887 def align_string_based_vulns(severity):
888 severities = ['info','low', 'med', 'high', 'critical']
889 for sev in severities:
890 if severity[0:3] in sev:
891 return sev
892 return severity
893
894 severity = align_string_based_vulns(severity)
895
896 # Transform numeric severity into desc severity
897 numeric_severities = { '0' : 'info',
898 '1' : 'low',
899 '2' : 'med',
900 '3' : 'high',
901 "4" : 'critical' }
902
903
904 if not severity in numeric_severities.values():
905 severity = numeric_severities.get(severity, 'unclassified')
906
907 return severity
908
909 def updateID(self):
910 self._id = get_hash([self.name, self._desc])
911 self._prependParentId()
912
913 def _setDesc(self, desc):
914 self._desc = desc
915
916 #@save
917 @updateLocalMetadata
918 def updateAttributes(self, name=None, desc=None, severity=None, resolution=None, refs=None):
919 if name is not None:
920 self.setName(name)
921 if desc is not None:
922 self.setDescription(desc)
923 if resolution is not None:
924 self.setResolution(resolution)
925 if severity is not None:
926 self.severity = self.standarize(severity)
927 if refs is not None:
928 self.refs = refs
929
930 def _getDesc(self):
931 #return self._desc.getvalue()
932 return self._desc
933
934 desc = property(_getDesc, _setDesc)
935
936 def setDesc(self, desc):
937 self._desc = desc
938
939 def getDesc(self):
940 return self._desc
941
942 def getDescription(self):
943 return self.getDesc()
944
945 def setDescription(self, desc):
946 self.setDesc(desc)
947
948 def setResolution(self, resolution):
949 self.resolution = resolution
950
951 def getResolution(self):
952 return self.resolution
953
954 def getSeverity(self):
955 return self.severity
956
957 def setSeverity(self, severity):
958 self.severity = self.standarize(severity)
959
960 def getRefs(self):
961 return self.refs
962
963 def setRefs(self, refs):
964 if isinstance(refs, list):
965 self.refs.extend(refs)
966 elif ref is not None:
967 self.refs.append(refs)
968
969 def setData(self, data):
970 self.data = data
971
972 def getData(self):
973 return self.data
974
975
976 def __str__(self):
977 return "vuln id:%s - %s" % (self.id, self.name)
978
979 def __repr__(self):
980 return self.__str__()
981
982 def getSeverity(self):
983 return self.severity
984
985 #-------------------------------------------------------------------------------
986 class ModelObjectVulnWeb(ModelObjectVuln):
987 """
988 Simple class to store vulnerability web about any object.
989 This Vuln support path, hostname, request and response
990 parent will be a reference to the ModelObjectVuln being commented.
991 """
992 class_signature = "VulnerabilityWeb"
993
994 def __init__(self, name="",desc="", website="", path="", ref=None, severity="", resolution="", request="", response="",
995 method="",pname="", params="",query="",category="", parent_id=None):
996 """
997 The parameters ref can be a single value or a list with values
998 """
999 ModelObjectVuln.__init__(self, name,desc, ref, severity, resolution, parent_id)
1000 self.path = path
1001 self.website = website
1002 self.request = request
1003 self.response = response
1004 self.method = method
1005 self.pname = pname
1006 self.params = params
1007 self.query = query
1008 self.category = category
1009
1010 def updateID(self):
1011 self._id = get_hash([self.name, self.website, self.path, self.desc ])
1012 self._prependParentId()
1013
1014 def getPath(self):
1015 return self.path
1016
1017 def setPath(self, path):
1018 self.path = path
1019
1020 def getWebsite(self):
1021 return self.website
1022
1023 def setWebsite(self, website):
1024 self.website = website
1025
1026 def getRequest(self):
1027 return self.request
1028
1029 def setRequest(self, request):
1030 self.request = request
1031
1032 def getResponse(self):
1033 return self.response
1034
1035 def setResponse(self, response):
1036 self.response = response
1037
1038 def getMethod(self):
1039 return self.method
1040
1041 def setMethod(self, method):
1042 self.method = method
1043
1044 def getPname(self):
1045 return self.pname
1046
1047 def setPname(self, pname):
1048 self.pname = pname
1049
1050 def getParams(self):
1051 return self.params
1052
1053 def setParams(self, params):
1054 self.params = params
1055
1056 def getQuery(self):
1057 return self.query
1058
1059 def setQuery(self, query):
1060 self.query = query
1061
1062 def getCategory(self):
1063 return self.category
1064
1065 def setCategory(self, category):
1066 self.category = category
1067
1068 #@save
1069 @updateLocalMetadata
1070 def updateAttributes(self, name=None, desc=None, website=None, path=None, refs=None,
1071 severity=None, resolution=None, request=None,response=None, method=None,
1072 pname=None, params=None, query=None, category=None):
1073 super(ModelObjectVulnWeb, self).updateAttributes(name, desc, severity, resolution, refs)
1074 if website is not None:
1075 self.website = website
1076 if path is not None:
1077 self.path = path
1078 if request is not None:
1079 self.request = request
1080 if response is not None:
1081 self.response = response
1082 if method is not None:
1083 self.method = method
1084 if pname is not None:
1085 self.pname = pname
1086 if params is not None:
1087 self.params = params
1088 if query is not None:
1089 self.query = query
1090 if category is not None:
1091 self.category = category
1092
1093
1094 #-------------------------------------------------------------------------------
1095 class ModelObjectCred(ModelLeaf):
1096 """
1097 Simple class to store credentials about any object.
1098 id will be used to number credentials (based on a counter on the object being commented)
1099 parent will be a reference to the object being commented.
1100 To assing new password this:
1101 >>> cred.password = "foobar"
1102 to append text + or += operators can be used (no need to use password property):
1103 >>> cred += " hello world!"
1104 """
1105 class_signature = "Cred"
1106
1107 def __init__(self, username="", password="", parent_id=None):
1108 ModelLeaf.__init__(self, parent_id)
1109 self._username = str(username)
1110 self._password = str(password)
1111
1112 def updateID(self):
1113 self._id = get_hash([self._username, self._password])
1114 self._prependParentId()
1115
1116 def setPassword(self, password):
1117 self._password = str(password)
1118
1119 def getPassword(self):
1120 return self._password
1121
1122 def getUsername(self):
1123 return self._username
1124
1125 def setUsername(self, username):
1126 self._username = str(username)
1127
1128 password = property(getPassword, setPassword)
1129
1130 username = property(getUsername, setUsername)
1131
1132 #@save
1133 @updateLocalMetadata
1134 def updateAttributes(self, username=None, password=None):
1135 if username is not None:
1136 self.setUsername(username)
1137 if password is not None:
1138 self.setPassword(password)
1139
1140 class TreeWordsTries(object):
1141 instance = None
1142 END = '_end_'
1143 root = dict()
1144 FOUND = True
1145
1146 def __init__(self):
1147 self.partial_match = False
1148 self.partial_match_dict = {}
1149 self.cur_idx = 0
1150
1151 def addWord(self, word):
1152 current_dict = self.root
1153 for letter in word:
1154 current_dict = current_dict.setdefault(letter, {})
1155
1156 current_dict = current_dict.setdefault(self.END, self.END)
1157
1158 def getWordsInText(self, text):
1159 current_dict = self.root
1160 list_of_word = list()
1161 w = ''
1162 for letter in text:
1163 if letter in current_dict:
1164 current_dict = current_dict[letter]
1165 w += letter
1166 elif self.END in current_dict:
1167 list_of_word.append(w)
1168 current_dict = self.root
1169 w = ''
1170 else:
1171 current_dict = self.root
1172 w = ''
1173
1174 if self.END in current_dict:
1175 list_of_word.append(w)
1176
1177 return list_of_word
1178
1179
1180 def isInTries(self, word):
1181 current_dict = self.root
1182
1183 if word is None:
1184 return False
1185
1186 for letter in word:
1187 if letter in current_dict:
1188 current_dict = current_dict[letter]
1189 else:
1190 return not self.FOUND
1191 else:
1192 if self.END in current_dict:
1193 return self.FOUND
1194 else:
1195 return False
1196
1197 def __new__(cls, *args, **kargs):
1198 if cls.instance is None:
1199 cls.instance = object.__new__(cls, *args, **kargs)
1200 return cls.instance
1201
1202 def removeWord(self, word):
1203 previous_dict = None
1204 current_dict = self.root
1205 last_letter = ''
1206
1207 if not self.isInTries(word):
1208 return
1209
1210 for letter in word:
1211 if letter in current_dict:
1212 if not previous_dict:
1213 previous_dict = current_dict
1214 last_letter = letter
1215 if len(current_dict.keys()) != 1:
1216 previous_dict = current_dict
1217 last_letter = letter
1218 current_dict = current_dict[letter]
1219 else:
1220 if self.END in current_dict:
1221 previous_dict.pop(last_letter)
1222
1223 def clear(self):
1224 self.root = dict()
1225 self.FOUND = True
1226
1227
1228
1229 #-------------------------------------------------------------------------------
1230 # taken from http://code.activestate.com/recipes/576477-yet-another-signalslot-implementation-in-python/
1231 # under MIT License
1232 #TODO: decide if we are going to use this...
1233 class Signal(object):
1234 """
1235 used to handle signals between several objects
1236 """
1237 def __init__(self):
1238 self.__slots = WeakValueDictionary()
1239
1240 def __call__(self, *args, **kargs):
1241 for key in self.__slots:
1242 func, _ = key
1243 func(self.__slots[key], *args, **kargs)
1244
1245 def connect(self, slot):
1246 key = (slot.im_func, id(slot.im_self))
1247 self.__slots[key] = slot.im_self
1248
1249 def disconnect(self, slot):
1250 key = (slot.im_func, id(slot.im_self))
1251 if key in self.__slots:
1252 self.__slots.pop(key)
1253
1254 def clear(self):
1255 self.__slots.clear()
1256
1257 #-------------------------------------------------------------------------------
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 """
7 Contains base classes used to represent the application model
8 and some other common objects and functions used in the model
9 """
10 import sys
11 import os
12 import traceback
13 import threading
14 import SimpleXMLRPCServer
15 import xmlrpclib
16 from utils.decorators import updateLocalMetadata
17 import json
18 import model
19 from conflict import ConflictUpdate
20 from model.diff import ModelObjectDiff
21
22 try:
23 import model.api as api
24 except AttributeError:
25 import api
26 from utils.common import *
27
28 #----------- Metadata history for timeline support, prob. we should move this out model common
29
30 from time import time
31 import cPickle as pickle
32 from config.configuration import getInstanceConfiguration
33
34 class MetadataUpdateActions(object):
35 """Constants for the actions made on the update"""
36 UNDEFINED = -1
37 CREATE = 0
38 UPDATE = 1
39
40 class Metadata(object):
41 """To save information about the modification of ModelObjects.
42 All members declared public as this is only a wrapper"""
43
44 class_signature = "Metadata"
45
46 def __init__(self, user):
47 self.creator = user
48 self.owner = user
49 self.create_time = time()
50 self.update_time = time()
51 self.update_user = user
52 self.update_action = MetadataUpdateActions.CREATE
53 self.update_controller_action = self.__getUpdateAction()
54
55 def toDict(self):
56 return self.__dict__
57
58 def fromDict(self, dictt):
59 for k, v in dictt.items():
60 setattr(self, k, v)
61 return self
62
63
64 def update(self, user, action = MetadataUpdateActions.UPDATE):
65 """Update the local metadata giving a user and an action.
66 Update time gets modified to the current system time"""
67 self.update_user = user
68 self.update_time = time()
69 self.update_action = action
70
71 self.update_controller_action = self.__getUpdateAction()
72
73 # api.devlog("Updating object (%s) metadata for user: (%s), utime = (%.4f), action=(%d), controller action = (%s)"
74 # % (self, self.update_user, self.update_time, self.update_action, self.update_controller_action))
75
76 def __getUpdateAction(self):
77 """This private method grabs the stackframes in look for the controller
78 call that generated the update"""
79
80 l_strace = traceback.extract_stack(limit = 10)
81 controller_funcallnames = [ x[2] for x in l_strace if "controller" in x[0] ]
82
83 if controller_funcallnames:
84 return "ModelControler." + " ModelControler.".join(controller_funcallnames)
85 return "No model controller call"
86
87
88 class ModelObject(object):
89 """
90 This is the base class for every object we need to represent in the
91 system (like hosts, services, etc)
92 It defines some generic methods to handle internal attributes that are
93 dictionaries. It also has generic methods to deal with notes & vulns
94 since every object could have them.
95 """
96 # this static attribute used with a factory
97 class_signature = "ModelObject"
98
99 def __init__(self, parent_id=None):
100 self._name = ""
101 self._id = None
102 self._parent_id = parent_id
103 self._parent = None
104
105 self.owner = api.getLoggedUser()
106 self._metadata = Metadata(self.owner)
107
108 # indicates if object was owned somehow
109 # we could use this property to put a different color on the GUI
110 self._is_owned = False
111
112 # a custom description given by the user
113 # this can be used to explain the purpose of the object
114 self.description = ""
115
116 self.publicattrs = {'Description':'description',
117 'Name':'getName','Owned':'isOwned'
118 }
119
120 self.publicattrsrefs = {'Description': '_description',
121 'Name': '_name','Owned': '_is_owned'
122 }
123
124 self._updatePublicAttributes()
125
126 #TODO: I think notes and vulns should be a dict
127 self._notes = {}
128 self._vulns = {}
129 self._creds = {}
130 self.evidences = []
131
132 self.updates = []
133
134 def accept(self, visitor):
135 visitor.visit(self)
136
137 def defaultValues(self):
138 return [-1, 0, '', 'unknown', None, [], {}]
139
140 def __getAttribute(self, key):
141 """ Looks for the attribute beneth the public attribute dict """
142 return self.publicattrsrefs.get(key)
143
144 def propertyTieBreaker(self, key, prop1, prop2):
145 """ Breakes the conflict between two properties. If either of them
146 is a default value returns the true and only.
147 If neither returns the default value.
148 If conflicting returns a tuple with the values """
149 if prop1 in self.defaultValues(): return prop2
150 elif prop2 in self.defaultValues(): return prop1
151 elif self.tieBreakable(key): return self.tieBreak(key, prop1, prop2)
152 else: return (prop2, prop1)
153
154 def tieBreakable(self, key):
155 return False
156
157 def tieBreak(self, key, prop1, prop2):
158 return None
159
160 def addUpdate(self, newModelObject):
161 conflict = False
162 diff = ModelObjectDiff(self, newModelObject)
163 for k, v in diff.getPropertiesDiff().items():
164 attribute = self.__getAttribute(k)
165 prop_update = self.propertyTieBreaker(attribute, *v)
166 if isinstance(prop_update, tuple):
167 conflict = True
168 else:
169 setattr(self, attribute, prop_update)
170 if conflict:
171 self.updates.append(ConflictUpdate(self, newModelObject))
172 return conflict
173
174 def needs_merge(self, new_obj):
175 return ModelObjectDiff(self, new_obj).existDiff()
176
177 def getUpdates(self):
178 return self.updates
179
180 def updateResolved(self, update):
181 self.updates.remove(update)
182
183
184 # IMPORTANT: all delete methods are considered FULL delete
185 # this means it will delete the reference from host and all
186 # other objects containing them
187 def _getValueByID(self, attrName, ID):
188 """
189 attribute passed as a parameter MUST BE a dictionary indexed with a
190 string ID
191 if id is found as a part of a key it returns the object
192 it returns None otherwise
193 """
194 if ID:
195 hash_id = get_hash([ID])
196 ref = self.__getattribute__(attrName)
197 # we are assuming the value is unique inside the object ID's
198 for key in ref:
199 #XXX: this way of checking the ids doesn't allow us to use a real hash as key
200 # because we are checking if "id" is part of the key... not a good way of
201 # dealing with this...
202 if hash_id == key or ID == key:
203 return ref[key]
204 # if id (hash) was not found then we try with element names
205 for element in ref.itervalues():
206 #if id in element.name:
207 if ID == element.name:
208 return element
209 return None
210
211
212 def _addValue(self, attrName, newValue, setparent = False, update = False):
213 # attribute passed as a parameter MUST BE the name
214 # of an internal attribute which is a dictionary indexed
215 # with a string ID
216 valID = newValue.getID()
217 ref = self.__getattribute__(attrName)
218 if valID not in ref or update:
219 ref[valID] = newValue
220 if setparent:
221 newValue.setParent(self)
222 return True
223 #return not update
224 return False
225
226
227 def _updatePublicAttributes(self):
228 # can be overriden if needed
229 pass
230
231 def setID(self, ID=None):
232 if ID is None:
233 self.updateID()
234 else:
235 self._id = ID
236 return self._id
237
238 def updateID(self):
239 raise NotImplementedError("This should be overwritten")
240
241 def _prependParentId(self):
242 if self._parent_id:
243 self._id = '.'.join((self._parent_id, self.getID()))
244
245 def getID(self):
246 if self._id is None:
247 self.updateID()
248 return self._id
249
250 id = property(getID, setID)
251
252 def getMetadata(self):
253 """Returns the current metadata of the object"""
254 return self._metadata
255
256 def setMetadata(self, metadata):
257 self._metadata = metadata
258
259 def getMetadataHistory(self):
260 """Returns the current metadata of the object"""
261 return self._metadataHistory
262
263 def updateMetadata(self):
264 """ We are only saving the previous state so the newst is not available"""
265 self.getMetadata().update(self.owner)
266 # self.getMetadataHistory().pushMetadataForId(self.getID(), self.getMetadata())
267
268 def getHost(self):
269 #recursive method to recover the Host root
270 if self.class_signature == "Host":
271 return self
272 return self.getParent().getHost()
273
274 def setName(self, name):
275 self._name = name
276
277 def getName(self):
278 return self._name
279
280 name = property(getName, setName)
281
282 def setDescription(self, description):
283 self._description = description
284
285 def getDescription(self):
286 return self._description
287
288 description = property(getDescription, setDescription)
289
290 def isOwned(self):
291 return self._is_owned
292
293 def setOwned(self, owned=True):
294 self._is_owned = owned
295
296 def getOwner(self):
297 return self.owner
298
299 def setOwner(self, owner=None):
300 self.owner = owner
301
302 #@save
303 def setParent(self, parent):
304 self._parent = parent
305
306 def getParent(self):
307 return self._parent
308
309 parent = property(getParent, setParent)
310
311 #TODO: this should be removed and we could use some class
312 # based on dict to implement this
313
314
315 def _delValue(self, attrName, valID):
316 # attribute passed as a parameter MUST BE the name
317 # of an internal attribute which is a dictionary indexed
318 # with a string ID
319 api.devlog("(%s)._delValue(%s, %s)" % (self, attrName, valID))
320 ref = self.__getattribute__(attrName)
321 api.devlog("ref.keys() = %s" % ref.keys())
322 if valID in ref:
323 val = ref[valID]
324 del ref[valID]
325 val.delete()
326 return True
327
328 hash_id = get_hash([valID])
329 if hash_id in ref:
330 val = ref[hash_id]
331 del ref[hash_id]
332 val.delete()
333 return True
334
335 for element in ref.itervalues():
336 if valID == element.name:
337 val = ref[element.getID()]
338 del ref[element.getID()]
339 val.delete()
340 return True
341
342 # none of the ids were found
343 return False
344
345 def _delAllValues(self, attrName):
346 ref = self.__getattribute__(attrName)
347 try:
348 ref.clear()
349 return True
350 except Exception:
351 return False
352
353 #@delete
354 def delete(self):
355 del self
356
357 def _getAllValues(self, attrName, mode = 0):
358 """
359 attribute passed as a parameter MUST BE a dictionary indexed with a
360 string ID
361 return all values in the dictionary
362 mode = 0 returns a list of objects
363 mode = 1 returns a dictionary of objects with their id as key
364 """
365 ref = self.__getattribute__(attrName)
366 if mode:
367 return ref
368 else:
369 return sorted(ref.values())
370
371 def _getAllIDs(self, attrName):
372 ref = self.__getattribute__(attrName)
373 return ref.keys()
374
375 def _getValueCount(self, attrName):
376 ref = self.__getattribute__(attrName)
377 return len(ref)
378
379 def __repr__(self):
380 return "<ModelObject %s at 0x%x>" % (self.__class__.__name__, id(self))
381
382 def __str__(self):
383 return "<ModelObject %s ID = %s at 0x%x>" % (self.__class__.__name__, self._id, id(self))
384
385 #notes
386 @updateLocalMetadata
387 def addNote(self, newNote, update=False, setparent=True):
388 self.addChild(newNote)
389 return True
390
391 def newNote(self, name, text):
392 note = ModelObjectNote(name, text, self)
393 self.addNote(note)
394
395 @updateLocalMetadata
396 def delNote(self, noteID):
397 self.deleteChild(noteID)
398 return True
399
400 def getNotes(self):
401 return self.getChildsByType(ModelObjectNote.__name__)
402
403 def setNotes(self, notes):
404 self._addChildsDict(notes)
405
406 def getNote(self, noteID):
407 return self.findChild(noteID)
408
409 def notesCount(self):
410 return len(self._notes.values())
411
412 #Vulnerability
413 @updateLocalMetadata
414 def addVuln(self, newVuln, update=False, setparent=True):
415 self.addChild(newVuln)
416 return True
417
418 @updateLocalMetadata
419 def delVuln(self, vulnID):
420 self.deleteChild(vulnID)
421 return True
422
423 def getVulns(self):
424 return self.getChildsByType(ModelObjectVuln.__name__) + self.getChildsByType(ModelObjectVulnWeb.__name__)
425
426 def setVulns(self, vulns):
427 self._addChildsDict(vulns)
428
429 def getVuln(self, vulnID):
430 return self.findChild(vulnID)
431
432 def vulnsCount(self):
433 return len(self._vulns.values())
434
435 def vulnsToDict(self):
436 d = []
437 for vuln in self._vulns.values():
438 d.append(vuln.toDictFull())
439 return d
440
441 @updateLocalMetadata
442 def delCred(self, credID):
443 return self._delValue("_creds", credID)
444
445 def getCreds(self):
446 return self.getChildsByType(ModelObjectCred.__name__)
447
448 def setCreds(self, creds):
449 self._addChildsDict(creds)
450
451 def getCred(self, credID):
452 return self._getValueByID("_creds", credID)
453
454 def credsToDict(self):
455 d = []
456 for cred in self._creds.values():
457 d.append(cred.toDictFull())
458 return d
459
460
461 def credsCount(self):
462 return len(self._creds.values())
463
464 def __getClassname(self, val):
465 supported = factory.listModelObjectTypes()
466 return filter(lambda x: val.lower().replace('_', '')[:-1] in x.lower(), supported)[0]
467
468 def _asdict(self):
469 return self.toDictFull()
470
471 def ancestors_path(self):
472 if self.getParent() is None:
473 return str(self.getID())
474 return ".".join([self.getParent().ancestors_path()] + [str(self.getID())])
475
476 def _addChildsDict(self, dictt):
477 [self.addChild(v) for k, v in dictt.items()]
478
479
480 class ModelComposite(ModelObject):
481 """ Model Objects Composite Abstract Class """
482
483 def __init__(self, parent_id=None):
484 ModelObject.__init__(self, parent_id)
485 self.childs = {}
486
487 def addChild(self, model_object):
488 model_object.setParent(self)
489 self.childs[model_object.getID()] = model_object
490
491 def getChilds(self):
492 return self.childs
493
494 def getChildsByType(self, signature):
495 return [c for c in self.childs.values()
496 if c.__class__.__name__ == signature]
497
498 def deleteChild(self, iid):
499 del self.childs[iid]
500
501 def findChild(self, iid):
502 return self.childs.get(iid)
503
504 class ModelLeaf(ModelObject):
505 def __init__(self, parent_id=None):
506 ModelObject.__init__(self, parent_id)
507
508 def getChildsByType(self, signature):
509 return []
510
511 def getChilds(self):
512 return {}
513
514 #-------------------------------------------------------------------------------
515 #TODO: refactor this class to make it generic so this can be used also for plugins
516 # then create a subclass and inherit the generic factory
517 class ModelObjectFactory(object):
518 """
519 Factory to creat any ModelObject type
520 """
521 def __init__(self):
522 self._registered_objects = dict()
523
524 def register(self, model_object):
525 """registers a class into the factory"""
526 self._registered_objects[model_object.class_signature] = model_object
527
528 def listModelObjectClasses(self):
529 """returns a list of registered classes"""
530 return self._registered_objects.values()
531
532 def getModelObjectClass(self, name):
533 """get the class for a particular object typename"""
534 return self._registered_objects[name]
535
536 def listModelObjectTypes(self):
537 """returns an array with object typenames the factory is able to create"""
538 names = self._registered_objects.keys()
539 names.sort()
540 return names
541
542 def generateID(self, classname, parent_id=None, **objargs):
543 tmpObj = self._registered_objects[classname](**objargs)
544 if parent_id:
545 return '.'.join([parent_id, tmpObj.getID()])
546 return tmpObj.getID()
547
548 def createModelObject(self, classname, object_name=None, **objargs):
549 if classname in self._registered_objects:
550 if object_name is not None:
551 tmpObj = self._registered_objects[classname](object_name,**objargs)
552 return tmpObj
553 else:
554 raise Exception("Object name parameter missing. Cannot create object class: %s" % classname)
555 else:
556 raise Exception("Object class %s not registered in factory. Cannot create object." % classname)
557
558 #-------------------------------------------------------------------------------
559 # global reference kind of a singleton
560 factory = ModelObjectFactory()
561
562 #-------------------------------------------------------------------------------
563
564 class CustomXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
565
566 def __init__(self, *args, **kwargs):
567 SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.__init__(self, *args, **kwargs)
568
569 def handle(self):
570 try:
571 api.devlog("-" * 60)
572 api.devlog("[XMLRPCHandler] - request = %s" % str(self.request))
573 api.devlog("[XMLRPCHandler] - client_address = %s" % str(self.client_address))
574 api.devlog("[XMLRPCHandler] - server = %s" % str(self.server))
575 api.devlog("-" * 60)
576 SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.handle(self)
577 except Exception:
578 api.devlog("[XMLRPCHandler] - An error ocurred while handling a request\n%s" % traceback.format_exc())
579
580 def do_POST(self):
581 """
582 Handles the HTTP POST request.
583 Attempts to interpret all HTTP POST requests as XML-RPC calls,
584 which are forwarded to the server's _dispatch method for handling.
585
586 This is a copy of the original do_POST, but it sends information about
587 the client calling the server to the marshaled dispatch. This info
588 can be later passed to the server
589 """
590
591 # Check that the path is legal
592 if not self.is_rpc_path_valid():
593 self.report_404()
594 return
595
596 try:
597 # Get arguments by reading body of request.
598 # We read this in chunks to avoid straining
599 # socket.read(); around the 10 or 15Mb mark, some platforms
600 # begin to have problems (bug #792570).
601 max_chunk_size = 10*1024*1024
602 size_remaining = int(self.headers["content-length"])
603 L = []
604 while size_remaining:
605 chunk_size = min(size_remaining, max_chunk_size)
606 L.append(self.rfile.read(chunk_size))
607 size_remaining -= len(L[-1])
608 data = ''.join(L)
609
610 # In previous versions of SimpleXMLRPCServer, _dispatch
611 # could be overridden in this class, instead of in
612 # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
613 # check to see if a subclass implements _dispatch and dispatch
614 # using that method if present.
615 response = self.server._marshaled_dispatch(
616 data, getattr(self, '_dispatch', None)
617 )
618 except Exception, e: # This should only happen if the module is buggy
619 # internal error, report as HTTP server error
620 self.send_response(500)
621
622 # Send information about the exception if requested
623 if hasattr(self.server, '_send_traceback_header') and \
624 self.server._send_traceback_header:
625 self.send_header("X-exception", str(e))
626 self.send_header("X-traceback", traceback.format_exc())
627
628 self.end_headers()
629 else:
630 # got a valid XML RPC response
631 self.send_response(200)
632 self.send_header("Content-type", "text/xml")
633 self.send_header("Content-length", str(len(response)))
634 self.end_headers()
635 self.wfile.write(response)
636
637 # shut down the connection
638 self.wfile.flush()
639 self.connection.shutdown(1)
640 #-------------------------------------------------------------------------------
641 # custom XMLRPC server with stopping function
642 #TODO: check http://epydoc.sourceforge.net/stdlib/SimpleXMLRPCServer.SimpleXMLRPCServer-class.html
643 # see if there is a way to know the ip caller
644 # looks like the request handler can give us that info
645 # http://epydoc.sourceforge.net/stdlib/BaseHTTPServer.BaseHTTPRequestHandler-class.html#address_string
646 #
647
648 class XMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer, threading.Thread):
649 """
650 Stoppable XMLRPC Server with custom dispatch to send over complete traceback
651 in case of exception.
652 """
653 def __init__(self, *args, **kwargs):
654 threading.Thread.__init__(self)
655 SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, requestHandler = CustomXMLRPCRequestHandler, allow_none = True, *args,**kwargs)
656 self._stop = False
657 # set timeout for handle_request. If we don't the server will hang
658 self.timeout = 2
659
660 def run(self):
661 self.serve_forever()
662 api.devlog("serve_forever ended")
663 return
664
665 # overloaded method to be able to stop server
666 def serve_forever(self):
667 while not self._stop:
668 self.handle_request()
669 api.devlog("server forever stopped by flag")
670
671 def stop_server(self):
672 api.devlog("server stopping...")
673 self._stop = True
674
675 # The default dispatcher does not send across the whole stack trace.
676 # Only type and value are passed back. The client has no way of knowing
677 # the exact place where error occurred in the server (short of some
678 # other means such as server logging). This dispatcher sends the whole
679 # stack trace.
680 def _dispatch(self, method, params):
681 """Dispatches the XML-RPC method.
682
683 XML-RPC calls are forwarded to a registered function that
684 matches the called XML-RPC method name. If no such function
685 exists then the call is forwarded to the registered instance,
686 if available.
687
688 If the registered instance has a _dispatch method then that
689 method will be called with the name of the XML-RPC method and
690 its parameters as a tuple
691 e.g. instance._dispatch('add',(2,3))
692
693 If the registered instance does not have a _dispatch method
694 then the instance will be searched to find a matching method
695 and, if found, will be called.
696
697 Methods beginning with an '_' are considered private and will
698 not be called.
699 """
700
701 func = None
702 try:
703 # check to see if a matching function has been registered
704 func = self.funcs[method]
705 except KeyError:
706 if self.instance is not None:
707 # check for a _dispatch method
708 if hasattr(self.instance, '_dispatch'):
709 return self.instance._dispatch(method, params)
710 else:
711 # call instance method directly
712 try:
713 func = SimpleXMLRPCServer.resolve_dotted_attribute(
714 self.instance,
715 method,
716 self.allow_dotted_names
717 )
718 except AttributeError:
719 pass
720
721 if func is not None:
722 try:
723 # since we are using a keyword xmlrpc proxy this is sending
724 # the info comes in form of args and kwargs
725 # so params has 2 items, the first being a list or tuple
726 # and the second a dictionary
727 if len(params) == 2 and isinstance(params[1],dict) and\
728 ( isinstance(params[0],list) or isinstance(params[0],tuple) ) :
729 return func(*params[0], **params[1])
730 else:
731 # this is the default way in case a normal xmlrpclib.ServerProxy is used
732 return func(*params)
733 except Exception:
734 # extended functionality to let the client have the full traceback
735 msg = traceback.format_exc()
736 raise xmlrpclib.Fault(1, msg)
737 else:
738 raise Exception('method "%s" is not supported' % method)
739
740
741 def _marshaled_dispatch(self, data, dispatch_method = None):
742 """Dispatches an XML-RPC method from marshalled (XML) data.
743
744 XML-RPC methods are dispatched from the marshalled (XML) data
745 using the _dispatch method and the result is returned as
746 marshalled data. For backwards compatibility, a dispatch
747 function can be provided as an argument (see comment in
748 SimpleXMLRPCRequestHandler.do_POST) but overriding the
749 existing method through subclassing is the prefered means
750 of changing method dispatch behavior.
751 """
752
753 try:
754 params, method = xmlrpclib.loads(data)
755
756 # generate response
757 if dispatch_method is not None:
758 response = dispatch_method(method, params)
759 else:
760 response = self._dispatch(method, params)
761 # wrap response in a singleton tuple
762 response = (response,)
763 response = xmlrpclib.dumps(response, methodresponse=1,
764 allow_none=self.allow_none, encoding=self.encoding)
765 except Fault, fault:
766 response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
767 encoding=self.encoding)
768 except Exception:
769 # report exception back to server
770 exc_type, exc_value, exc_tb = sys.exc_info()
771 response = xmlrpclib.dumps(
772 xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
773 encoding=self.encoding, allow_none=self.allow_none,
774 )
775
776 return response
777
778 #-------------------------------------------------------------------------------
779
780 class XMLRPCKeywordProxy(object):
781 """
782 custom XMLRPC Server Proxy capable of receiving keyword arguments
783 when calling remote methods
784 """
785 def __init__(self, *args, **kwargs):
786 self._xmlrpc_server_proxy = xmlrpclib.ServerProxy(*args, **kwargs)
787 def __getattr__(self, name):
788 call_proxy = getattr(self._xmlrpc_server_proxy, name)
789 def _call(*args, **kwargs):
790 return call_proxy(args, kwargs)
791 return _call
792
793
794
795 #-------------------------------------------------------------------------------
796 class ModelObjectNote(ModelComposite):
797 """
798 Simple class to store notes about any object.
799 id will be used to number notes (based on a counter on the object being commented)
800 parent will be a reference to the object being commented.
801 To assing new text this:
802 >>> note.text = "foobar"
803 to append text + or += operators can be used (no need to use text property):
804 >>> note += " hello world!"
805 """
806 class_signature = "Note"
807
808 def __init__(self, name="", text="", parent_id=None):
809 ModelComposite.__init__(self, parent_id)
810 self.name = str(name)
811 self._text = str(text)
812
813 def updateID(self):
814 self._id = get_hash([self.name, self._text])
815 self._prependParentId()
816
817 def _setText(self, text):
818 # clear buffer then write new text
819 # self._text.seek(0)
820 # self._text.truncate()
821 # self._text.write(text)
822 self._text = str(text)
823
824 def _getText(self):
825 # return self._text.getvalue()
826 return self._text
827
828 def getText(self):
829 # return self._text.getvalue()
830 return self._text
831
832 def setText(self, text):
833 # return self._text.getvalue()
834 self._text = str(text)
835
836 text = property(_getText, _setText)
837
838 #@save
839 @updateLocalMetadata
840 def updateAttributes(self, name=None, text=None):
841 if name is not None:
842 self.setName(name)
843 if text is not None:
844 self.text = text
845
846 def __str__(self):
847 return "%s: %s" % (self.name, self.text)
848
849 def __repr__(self):
850 return "%s: %s" % (self.name, self.text)
851
852
853 class ModelObjectVuln(ModelComposite):
854 class_signature = "Vulnerability"
855
856 def __init__(self, name="", desc="", ref=None, severity="", resolution="",
857 confirmed=False, parent_id=None):
858 """
859 The parameters refs can be a single value or a list with values
860 """
861 ModelComposite.__init__(self, parent_id)
862 self.name = name
863 self._desc = desc
864 self.data = ""
865 self.confirmed = confirmed
866 self.refs = []
867
868 if isinstance(ref, list):
869 self.refs.extend(ref)
870 elif ref is not None:
871 self.refs.append(ref)
872
873 # Severity Standarization
874 self.severity = self.standarize(severity)
875 self.resolution = resolution
876
877 def standarize(self, severity):
878 # Transform all severities into lower strings
879 severity = str(severity).lower()
880 # If it has info, med, high, critical in it, standarized to it:
881
882
883 def align_string_based_vulns(severity):
884 severities = ['info','low', 'med', 'high', 'critical']
885 for sev in severities:
886 if severity[0:3] in sev:
887 return sev
888 return severity
889
890 severity = align_string_based_vulns(severity)
891
892 # Transform numeric severity into desc severity
893 numeric_severities = { '0' : 'info',
894 '1' : 'low',
895 '2' : 'med',
896 '3' : 'high',
897 "4" : 'critical' }
898
899
900 if not severity in numeric_severities.values():
901 severity = numeric_severities.get(severity, 'unclassified')
902
903 return severity
904
905 def updateID(self):
906 self._id = get_hash([self.name, self._desc])
907 self._prependParentId()
908
909 def tieBreakable(self, key):
910 '''
911 If the confirmed property has a conflict, there's two possible reasons:
912 confirmed is false, and the new value is true => returns true
913 confirmed is true, and the new value is false => returns true
914 '''
915 if key == "confirmed":
916 return True
917 return False
918
919 def tieBreak(self, key, prop1, prop2):
920 if key == "confirmed":
921 return True
922 return (prop1, prop2)
923
924 def _setDesc(self, desc):
925 self._desc = desc
926
927 #@save
928 @updateLocalMetadata
929 def updateAttributes(self, name=None, desc=None, severity=None, resolution=None, refs=None):
930 if name is not None:
931 self.setName(name)
932 if desc is not None:
933 self.setDescription(desc)
934 if resolution is not None:
935 self.setResolution(resolution)
936 if severity is not None:
937 self.severity = self.standarize(severity)
938 if refs is not None:
939 self.refs = refs
940
941 def _getDesc(self):
942 return self._desc
943
944 desc = property(_getDesc, _setDesc)
945
946 def setDesc(self, desc):
947 self._desc = desc
948
949 def getDesc(self):
950 return self._desc
951
952 def getDescription(self):
953 return self.getDesc()
954
955 def setDescription(self, desc):
956 self.setDesc(desc)
957
958 def setResolution(self, resolution):
959 self.resolution = resolution
960
961 def getResolution(self):
962 return self.resolution
963
964 def getSeverity(self):
965 return self.severity
966
967 def setSeverity(self, severity):
968 self.severity = self.standarize(severity)
969
970 def getRefs(self):
971 return self.refs
972
973 def setRefs(self, refs):
974 if isinstance(refs, list):
975 self.refs.extend(refs)
976 elif ref is not None:
977 self.refs.append(refs)
978
979 def setData(self, data):
980 self.data = data
981
982 def getData(self):
983 return self.data
984
985 def setConfirmed(self, confirmed):
986 self.confirmed = confirmed
987
988 def getConfirmed(self):
989 return self.confirmed
990
991 def __str__(self):
992 return "vuln id:%s - %s" % (self.id, self.name)
993
994 def __repr__(self):
995 return self.__str__()
996
997
998 class ModelObjectVulnWeb(ModelObjectVuln):
999 """
1000 Simple class to store vulnerability web about any object.
1001 This Vuln support path, hostname, request and response
1002 parent will be a reference to the ModelObjectVuln being commented.
1003 """
1004 class_signature = "VulnerabilityWeb"
1005
1006 def __init__(self, name="", desc="", website="", path="", ref=None,
1007 severity="", resolution="", request="", response="",
1008 method="", pname="", params="", query="", category="",
1009 confirmed=False, parent_id=None):
1010 """
1011 The parameters ref can be a single value or a list with values
1012 """
1013 ModelObjectVuln.__init__(
1014 self, name, desc, ref, severity, resolution, confirmed,
1015 parent_id)
1016 self.path = path
1017 self.website = website
1018 self.request = request
1019 self.response = response
1020 self.method = method
1021 self.pname = pname
1022 self.params = params
1023 self.query = query
1024 self.category = category
1025
1026 def updateID(self):
1027 self._id = get_hash([self.name, self.website, self.path, self.desc ])
1028 self._prependParentId()
1029
1030 def getPath(self):
1031 return self.path
1032
1033 def setPath(self, path):
1034 self.path = path
1035
1036 def getWebsite(self):
1037 return self.website
1038
1039 def setWebsite(self, website):
1040 self.website = website
1041
1042 def getRequest(self):
1043 return self.request
1044
1045 def setRequest(self, request):
1046 self.request = request
1047
1048 def getResponse(self):
1049 return self.response
1050
1051 def setResponse(self, response):
1052 self.response = response
1053
1054 def getMethod(self):
1055 return self.method
1056
1057 def setMethod(self, method):
1058 self.method = method
1059
1060 def getPname(self):
1061 return self.pname
1062
1063 def setPname(self, pname):
1064 self.pname = pname
1065
1066 def getParams(self):
1067 return self.params
1068
1069 def setParams(self, params):
1070 self.params = params
1071
1072 def getQuery(self):
1073 return self.query
1074
1075 def setQuery(self, query):
1076 self.query = query
1077
1078 def getCategory(self):
1079 return self.category
1080
1081 def setCategory(self, category):
1082 self.category = category
1083
1084 #@save
1085 @updateLocalMetadata
1086 def updateAttributes(self, name=None, desc=None, website=None, path=None, refs=None,
1087 severity=None, resolution=None, request=None,response=None, method=None,
1088 pname=None, params=None, query=None, category=None):
1089 super(ModelObjectVulnWeb, self).updateAttributes(name, desc, severity, resolution, refs)
1090 if website is not None:
1091 self.website = website
1092 if path is not None:
1093 self.path = path
1094 if request is not None:
1095 self.request = request
1096 if response is not None:
1097 self.response = response
1098 if method is not None:
1099 self.method = method
1100 if pname is not None:
1101 self.pname = pname
1102 if params is not None:
1103 self.params = params
1104 if query is not None:
1105 self.query = query
1106 if category is not None:
1107 self.category = category
1108
1109
1110 #-------------------------------------------------------------------------------
1111 class ModelObjectCred(ModelLeaf):
1112 """
1113 Simple class to store credentials about any object.
1114 id will be used to number credentials (based on a counter on the object being commented)
1115 parent will be a reference to the object being commented.
1116 To assing new password this:
1117 >>> cred.password = "foobar"
1118 to append text + or += operators can be used (no need to use password property):
1119 >>> cred += " hello world!"
1120 """
1121 class_signature = "Cred"
1122
1123 def __init__(self, username="", password="", parent_id=None):
1124 ModelLeaf.__init__(self, parent_id)
1125 self._username = str(username)
1126 self._password = str(password)
1127
1128 def updateID(self):
1129 self._id = get_hash([self._username, self._password])
1130 self._prependParentId()
1131
1132 def setPassword(self, password):
1133 self._password = str(password)
1134
1135 def getPassword(self):
1136 return self._password
1137
1138 def getUsername(self):
1139 return self._username
1140
1141 def setUsername(self, username):
1142 self._username = str(username)
1143
1144 password = property(getPassword, setPassword)
1145
1146 username = property(getUsername, setUsername)
1147
1148 #@save
1149 @updateLocalMetadata
1150 def updateAttributes(self, username=None, password=None):
1151 if username is not None:
1152 self.setUsername(username)
1153 if password is not None:
1154 self.setPassword(password)
1155
1156 class TreeWordsTries(object):
1157 instance = None
1158 END = '_end_'
1159 root = dict()
1160 FOUND = True
1161
1162 def __init__(self):
1163 self.partial_match = False
1164 self.partial_match_dict = {}
1165 self.cur_idx = 0
1166
1167 def addWord(self, word):
1168 current_dict = self.root
1169 for letter in word:
1170 current_dict = current_dict.setdefault(letter, {})
1171
1172 current_dict = current_dict.setdefault(self.END, self.END)
1173
1174 def getWordsInText(self, text):
1175 current_dict = self.root
1176 list_of_word = list()
1177 w = ''
1178 for letter in text:
1179 if letter in current_dict:
1180 current_dict = current_dict[letter]
1181 w += letter
1182 elif self.END in current_dict:
1183 list_of_word.append(w)
1184 current_dict = self.root
1185 w = ''
1186 else:
1187 current_dict = self.root
1188 w = ''
1189
1190 if self.END in current_dict:
1191 list_of_word.append(w)
1192
1193 return list_of_word
1194
1195
1196 def isInTries(self, word):
1197 current_dict = self.root
1198
1199 if word is None:
1200 return False
1201
1202 for letter in word:
1203 if letter in current_dict:
1204 current_dict = current_dict[letter]
1205 else:
1206 return not self.FOUND
1207 else:
1208 if self.END in current_dict:
1209 return self.FOUND
1210 else:
1211 return False
1212
1213 def __new__(cls, *args, **kargs):
1214 if cls.instance is None:
1215 cls.instance = object.__new__(cls, *args, **kargs)
1216 return cls.instance
1217
1218 def removeWord(self, word):
1219 previous_dict = None
1220 current_dict = self.root
1221 last_letter = ''
1222
1223 if not self.isInTries(word):
1224 return
1225
1226 for letter in word:
1227 if letter in current_dict:
1228 if not previous_dict:
1229 previous_dict = current_dict
1230 last_letter = letter
1231 if len(current_dict.keys()) != 1:
1232 previous_dict = current_dict
1233 last_letter = letter
1234 current_dict = current_dict[letter]
1235 else:
1236 if self.END in current_dict:
1237 previous_dict.pop(last_letter)
1238
1239 def clear(self):
1240 self.root = dict()
1241 self.FOUND = True
1242
1243
1244
1245 #-------------------------------------------------------------------------------
1246 # taken from http://code.activestate.com/recipes/576477-yet-another-signalslot-implementation-in-python/
1247 # under MIT License
1248 #TODO: decide if we are going to use this...
1249 class Signal(object):
1250 """
1251 used to handle signals between several objects
1252 """
1253 def __init__(self):
1254 self.__slots = WeakValueDictionary()
1255
1256 def __call__(self, *args, **kargs):
1257 for key in self.__slots:
1258 func, _ = key
1259 func(self.__slots[key], *args, **kargs)
1260
1261 def connect(self, slot):
1262 key = (slot.im_func, id(slot.im_self))
1263 self.__slots[key] = slot.im_self
1264
1265 def disconnect(self, slot):
1266 key = (slot.im_func, id(slot.im_self))
1267 if key in self.__slots:
1268 self.__slots.pop(key)
1269
1270 def clear(self):
1271 self.__slots.clear()
1272
1273 #-------------------------------------------------------------------------------
470470 # qt and in the terminal. Ugly.
471471 msg = "A parent is needed for %s objects" % obj.class_signature
472472 getLogger(self).error(msg)
473 model.api.log(msg)
474473 return False
475474 dataMapper.save(obj)
476475 self.treeWordsTries.addWord(obj.getName())
856855 name, protocol=protocol, ports=ports, status=status,
857856 version=version, description=description, parent_id=parent_id)
858857
859 def newVuln(self, name, desc="", ref=None, severity="", resolution="", parent_id=None):
858 def newVuln(self, name, desc="", ref=None, severity="", resolution="",
859 confirmed=False, parent_id=None):
860860 return model.common.factory.createModelObject(
861861 model.common.ModelObjectVuln.class_signature,
862 name, desc=desc, ref=ref, severity=severity, resolution=resolution, parent_id=parent_id)
863
864 def newVulnWeb(self, name, desc="", ref=None, severity="", resolution="", website="",
865 path="", request="", response="", method="", pname="",
866 params="", query="", category="", parent_id=None):
862 name, desc=desc, ref=ref, severity=severity, resolution=resolution,
863 confirmed=confirmed, parent_id=parent_id)
864
865 def newVulnWeb(self, name, desc="", ref=None, severity="", resolution="",
866 website="", path="", request="", response="", method="",
867 pname="", params="", query="", category="", confirmed=False,
868 parent_id=None):
867869 return model.common.factory.createModelObject(
868870 model.common.ModelObjectVulnWeb.class_signature,
869871 name, desc=desc, ref=ref, severity=severity, resolution=resolution,
870872 website=website, path=path, request=request, response=response,
871873 method=method, pname=pname, params=params, query=query,
872 category=category, parent_id=parent_id)
874 category=category, confirmed=confirmed, parent_id=parent_id)
873875
874876 def newNote(self, name, text, parent_id=None):
875877 return model.common.factory.createModelObject(
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 #from model.common import factory
9 import model.common
10 from gui.notifier import NotificationCenter
11 from config.configuration import getInstanceConfiguration
12 import model.api
13 #from model.api import showDialog, showPopup
14
15 CONF = getInstanceConfiguration()
16
17
18 notification_center = NotificationCenter()
19 __the_mainapp = None
20 __model_controller = None
21
22 def setMainApp(ref):
23 global __the_mainapp
24 __the_mainapp = ref
25 notification_center.setUiApp(__the_mainapp)
26
27 def getMainApp():
28 global __the_mainapp
29 return __the_mainapp
30
31 def getMainWindow():
32 global __the_mainapp
33 return __the_mainapp.getMainWindow()
34
35 def postCustomEvent(event, receiver=None):
36 if receiver is None:
37 receiver = getMainWindow()
38 __the_mainapp.postEvent(receiver, event)
39
40 def sendCustomEvent(event, receiver=None):
41 if receiver is None:
42 receiver = getMainWindow()
43 __the_mainapp.sendEvent(receiver, event)
44
45 def setUpGUIAPIs(controller):
46 global __model_controller
47 __model_controller = controller
48
49
50 def registerWidget(widget):
51 if widget is not None:
52 notification_center.registerWidget(widget)
53
54
55 def deregisterWidget(widget):
56 if widget is not None:
57 notification_center.deregisterWidget(widget)
58
59
60 def createAndAddHost(name, os="Unknown", category=None, update=False, old_hostname=None):
61 host = model.api.newHost(name, os)
62 if addHost(host, category, update, old_hostname):
63 return host.getID()
64 return None
65
66
67 def createAndAddInterface(host_id, name = "", mac = "00:00:00:00:00:00",
68 ipv4_address = "0.0.0.0", ipv4_mask = "0.0.0.0",
69 ipv4_gateway = "0.0.0.0", ipv4_dns = [],
70 ipv6_address = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_prefix = "00",
71 ipv6_gateway = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_dns = [],
72 network_segment = "", hostname_resolution = []):
73 """
74 Creates a new interface object with the parameters provided and adds it to
75 the host selected.
76 If interface is successfuly created and the host exists, it returns the inteface id
77 It returns None otherwise
78 """
79 interface = model.api.newInterface(name, mac, ipv4_address, ipv4_mask, ipv4_gateway,
80 ipv4_dns, network_segment, hostname_resolution, parent_id=host_id)
81 if addInterface(host_id, interface):
82 return interface.getID()
83 return None
84
85
86 def createAndAddServiceToInterface(host_id, interface_id, name, protocol = "tcp?",
87 ports = [], status = "running", version = "unknown", description = ""):
88 service = model.api.newService(name, protocol, ports, status, version, description, parent_id=interface_id)
89 if addServiceToInterface(host_id, interface_id, service):
90 return service.getID()
91 return None
92
93
94 def createAndAddVulnToHost(host_id, name, desc, ref, severity="0", resolution=""):
95 vuln = model.api.newVuln(name, desc, ref, severity, resolution, parent_id=host_id)
96 if addVulnToHost(host_id, vuln):
97 return vuln.getID()
98 return None
99
100
101 def createAndAddVulnToInterface(host_id, interface_id, name, desc, ref, severity="0", resolution=""):
102 vuln = model.api.newVuln(name, desc, ref, severity, resolution, parent_id=interface_id)
103 if addVulnToInterface(host_id, interface_id, vuln):
104 return vuln.getID()
105 return None
106
107
108 def createAndAddVulnToService(host_id, service_id, name, desc, ref, severity="0", resolution=""):
109 vuln = model.api.newVuln(name, desc, ref, severity, resolution, parent_id=service_id)
110 if addVulnToService(host_id, service_id, vuln):
111 return vuln.getID()
112 return None
113
114
115 def createAndAddVulnWebToService(host_id, service_id, name, desc, website, path, ref=None,
116 severity="0", resolution="", request=None, response=None,
117 method=None,pname=None, params=None,query=None,category=None):
118 vuln = model.api.newVulnWeb(name, desc, website, path, ref, severity, resolution, request, response,
119 method, pname, params, query, category, parent_id=service_id)
120 if addVulnToService(host_id, service_id, vuln):
121 return vuln.getID()
122 return None
123
124
125 def createAndAddVuln(model_object, name, desc, ref=None, severity="0", resolution=""):
126 vuln = model.api.newVuln(name, desc, ref, severity, resolution, parent_id=model_object.getID())
127 if addVuln(model_object.getID(), vuln):
128 return vuln.getID()
129 return None
130
131
132 def createAndAddVulnWeb(model_object, name, desc, website, path, ref=None, severity="0", resolution="",
133 request=None, response=None, method=None, pname=None, params=None, query=None,
134 category=None):
135 vuln = model.api.newVulnWeb(name, desc, ref, severity, resolution, website, path, request, response,
136 method, pname, params, query, category, parent_id=model_object.getID())
137 if addVuln(model_object.getID(), vuln):
138 return vuln.getID()
139 return None
140
141
142 def createAndAddNoteToHost(host_id, name, text):
143 note = model.api.newNote(name, text, parent_id=host_id)
144 if addNoteToHost(host_id, note):
145 return note.getID()
146 return None
147
148
149 def createAndAddNoteToInterface(host_id, interface_id, name, text):
150 note = model.api.newNote(name, text, parent_id=interface_id)
151 if addNoteToInterface(host_id, interface_id, note):
152 return note.getID()
153 return None
154
155
156 def createAndAddNoteToService(host_id, service_id, name, text):
157 note = model.api.newNote(name, text, parent_id=service_id)
158 if addNoteToService(host_id, service_id, note):
159 return note.getID()
160 return None
161
162
163 def createAndAddNote(model_object, name, text):
164 note = model.api.newNote(name, text, parent_id=model_object.getID())
165 if addNote(model_object.getID(), note):
166 return note.getID()
167 return None
168
169
170 def createAndAddCred(model_object, username, password):
171 cred = model.api.newCred(username, password, parent_id=model_object.getID())
172 if addCred(model_object.getID(), cred):
173 return cred.getID()
174 return None
175
176
177 def createAndAddCredToHost(host_id, username, password):
178 cred = model.api.newCred(username, password, parent_id=host_id)
179 if addCredToHost(host_id, cred):
180 return cred.getID()
181 return None
182
183
184 def createAndAddCredToService(host_id, service_id, username, password):
185 cred = model.api.newCred(username, password, parent_id=service_id)
186 if addCredToService(host_id, service_id, cred):
187 return cred.getID()
188 return None
189
190
191 def addHost(host, category=None, update = False, old_hostname = None):
192 if host is not None:
193 __model_controller.addHostSYNC(host, category, update, old_hostname)
194 return True
195 return False
196
197 def addInterface(host_id, interface):
198 if interface is not None:
199 __model_controller.addInterfaceSYNC(host_id, interface)
200 return True
201 return False
202
203 def addApplication(host_id, application):
204 if application is not None:
205 __model_controller.addApplicationSYNC(host_id, application)
206 return True
207 return False
208
209 def addServiceToApplication(host_id, application_id, service):
210 if service is not None:
211 __model_controller.addServiceToApplicationSYNC(host_id, application_id, service)
212 return True
213 return False
214
215 def addServiceToInterface(host_id, interface_id, service):
216 if service is not None:
217 __model_controller.addServiceToInterfaceSYNC(host_id, interface_id, service)
218 return True
219 return False
220
221
222
223 def addVulnToHost(host_id, vuln):
224 if vuln is not None:
225 __model_controller.addVulnToHostSYNC(host_id, vuln)
226 return True
227 return False
228
229 def addVulnToInterface(host_id, interface_id, vuln):
230 if vuln is not None:
231 __model_controller.addVulnToInterfaceSYNC(host_id, interface_id, vuln)
232 return True
233 return False
234
235 def addVulnToApplication(host_id, application_id, vuln):
236 if vuln is not None:
237 __model_controller.addVulnToApplicationSYNC(host_id, application_id, vuln)
238 return True
239 return False
240
241 def addVulnToService(host_id, service_id, vuln):
242 if vuln is not None:
243 __model_controller.addVulnToServiceSYNC(host_id, service_id, vuln)
244 return True
245 return False
246
247 def addVuln(model_object_id, vuln):
248 if vuln is not None:
249 __model_controller.addVulnSYNC(model_object_id, vuln)
250 return True
251 return False
252
253
254
255 def addNoteToHost(host_id, note):
256 if note is not None:
257 __model_controller.addNoteToHostSYNC(host_id, note)
258 return True
259 return False
260
261 def addNoteToInterface(host_id, interface_id, note):
262 if note is not None:
263 __model_controller.addNoteToInterfaceSYNC(host_id, interface_id, note)
264 return True
265 return False
266
267 def addNoteToApplication(host_id, application_id, note):
268 if note is not None:
269 __model_controller.addNoteToApplicationSYNC(host_id, application_id, note)
270 return True
271 return False
272
273 def addNoteToService(host_id, service_id, note):
274 if note is not None:
275 __model_controller.addNoteToServiceSYNC(host_id, service_id, note)
276 return True
277 return False
278
279 def addNote(model_object_id, note):
280 if note is not None:
281 __model_controller.addNoteSYNC(model_object_id, note)
282 return True
283 return False
284
285
286 def addCred(model_object_id, cred):
287 if cred is not None:
288 __model_controller.addCredSYNC(model_object_id, cred)
289 return True
290 return False
291
292 def addCredToService(host_id, service_id, cred):
293 if cred is not None:
294 __model_controller.addCredToServiceSYNC(host_id, service_id, cred)
295 return True
296 return False
297
298 def addCredToHost(host_id, cred):
299 if cred is not None:
300 __model_controller.addCredToHostSYNC(host_id, cred)
301 return True
302 return False
303
304
305 def delHost(host_id):
306 __model_controller.delHostSYNC(host_id)
307 return True
308
309 def delApplication(host_id, application_id):
310 __model_controller.delApplicationSYNC(host_id, application_id)
311 return True
312
313 def delInterface(host_id, interface_id):
314 __model_controller.delInterfaceSYNC(host_id, interface_id)
315 return True
316
317 def delServiceFromHost(host_id, service_id):
318 __model_controller.delServiceFromHostSYNC(host_id, service_id)
319 return True
320
321 def delServiceFromInterface(host_id, interface_id, service_id):
322 __model_controller.delServiceFromInterfaceSYNC(host_id, interface_id, service_id)
323 return True
324
325 def delServiceFromApplication(host_id, application_id, service_id):
326 __model_controller.delServiceFromApplicationSYNC(host_id, application_id, service_id)
327 return True
328
329
330
331 def delVulnFromApplication(vuln, hostname, appname):
332 __model_controller.delVulnFromApplicationSYNC(hostname, appname, vuln)
333 return True
334
335 def delVulnFromInterface(vuln, hostname, intname):
336 __model_controller.delVulnFromInterfaceSYNC(hostname,intname, vuln)
337 return True
338
339 def delVulnFromHost(vuln, hostname):
340 __model_controller.delVulnFromHostSYNC(hostname,vuln)
341 return True
342
343
344 def delVulnFromService(vuln, hostname, srvname):
345 __model_controller.delVulnFromServiceSYNC(hostname,srvname, vuln)
346 return True
347
348 def delVuln(model_object_id, vuln_id):
349 __model_controller.delVulnSYNC(model_object_id, vuln_id)
350 return True
351
352
353
354 def delNoteFromApplication(note, hostname, appname):
355 __model_controller.delNoteFromApplicationSYNC(hostname, appname, note)
356 return True
357
358 def delNoteFromInterface(note, hostname, intname):
359 __model_controller.delNoteFromInterfaceSYNC(hostname,intname, note)
360 return True
361
362 def delNoteFromHost(note, hostname):
363 __model_controller.delNoteFromHostSYNC(hostname, note)
364 return True
365
366
367 def delNoteFromService(note, hostname, srvname):
368 __model_controller.delNoteFromServiceSYNC(hostname,srvname, note)
369 return True
370
371 def delNote(model_object_id, note_id):
372 __model_controller.delNoteSYNC(model_object_id, note_id)
373 return True
374
375
376 def delCred(model_object_id, cred_id):
377 __model_controller.delCredSYNC(model_object_id, cred_id)
378 return True
379
380 def delCredFromHost(cred, hostname):
381 __model_controller.delCredFromHostSYNC(hostname, cred)
382 return True
383
384
385 def delCredFromService(cred, hostname, srvname):
386 __model_controller.delCredFromServiceSYNC(hostname,srvname, cred)
387 return True
388
389
390
391
392
393 def editHost(host, name=None, description=None, os=None, owned=None):
394 __model_controller.editHostSYNC(host, name, description, os, owned)
395 return True
396
397 def editService(service, name=None, description=None, protocol=None, ports=None, status=None, version=None, owned=None):
398 __model_controller.editServiceSYNC(service, name, description, protocol, ports, status, version, owned)
399 return True
400
401 def editApplication(application, name, description, status, version, owned):
402 __model_controller.editApplicationSYNC(application, name, description, status, version, owned)
403 return True
404
405 def editInterface(interface, name=None, description=None, hostnames=None, mac=None, ipv4=None, ipv6=None, network_segment=None,
406 amount_ports_opened=None, amount_ports_closed=None, amount_ports_filtered=None, owned=None):
407 __model_controller.editInterfaceSYNC(interface, name, description, hostnames, mac, ipv4, ipv6, network_segment,
408 amount_ports_opened, amount_ports_closed, amount_ports_filtered, owned)
409 return True
410
411 def editNote(note, name=None, text=None):
412 __model_controller.editNoteSYNC(note, name, text)
413 return True
414
415 def editVuln(vuln, name=None, desc=None, severity=None, resolution=None, refs=None):
416 __model_controller.editVulnSYNC(vuln, name, desc, severity, resolution, refs)
417 return True
418
419 def editVulnWeb(vuln, name=None, desc=None, website=None, path=None, refs=None, severity=None, resolution=None,
420 request=None, response=None, method=None, pname=None, params=None, query=None, category=None):
421 __model_controller.editVulnWebSYNC(vuln, name, desc, website, path, refs, severity, resolution,
422 request, response, method, pname, params, query, category)
423 return True
424
425 def editCred(cred, username=None, password=None):
426 __model_controller.editCredSYNC(cred, username, password)
427 return True
428
429
430 def getParent(parent_id):
431 return __model_controller.find(parent_id)
432
433
434 def resolveConflicts():
435 __model_controller.resolveConflicts()
436
437 def resolveConflict(conflict, kwargs):
438 __model_controller.resolveConflict(conflict, kwargs)
439
440 def merge(host1, host2):
441 return __model_controller.merge(host1, host2)
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 #from model.common import factory
9 import model.common
10 from gui.notifier import NotificationCenter
11 from config.configuration import getInstanceConfiguration
12 import model.api
13 #from model.api import showDialog, showPopup
14
15 CONF = getInstanceConfiguration()
16
17
18 notification_center = NotificationCenter()
19 __the_mainapp = None
20 __model_controller = None
21
22 def setMainApp(ref):
23 global __the_mainapp
24 __the_mainapp = ref
25 notification_center.setUiApp(__the_mainapp)
26
27 def getMainApp():
28 global __the_mainapp
29 return __the_mainapp
30
31 def getMainWindow():
32 global __the_mainapp
33 return __the_mainapp.getMainWindow()
34
35 def postCustomEvent(event, receiver=None):
36 if receiver is None:
37 receiver = getMainWindow()
38 __the_mainapp.postEvent(receiver, event)
39
40 def sendCustomEvent(event, receiver=None):
41 if receiver is None:
42 receiver = getMainWindow()
43 __the_mainapp.sendEvent(receiver, event)
44
45 def setUpGUIAPIs(controller):
46 global __model_controller
47 __model_controller = controller
48
49
50 def registerWidget(widget):
51 if widget is not None:
52 notification_center.registerWidget(widget)
53
54
55 def deregisterWidget(widget):
56 if widget is not None:
57 notification_center.deregisterWidget(widget)
58
59
60 def createAndAddHost(name, os="Unknown", category=None, update=False, old_hostname=None):
61 host = model.api.newHost(name, os)
62 if addHost(host, category, update, old_hostname):
63 return host.getID()
64 return None
65
66
67 def createAndAddInterface(host_id, name = "", mac = "00:00:00:00:00:00",
68 ipv4_address = "0.0.0.0", ipv4_mask = "0.0.0.0",
69 ipv4_gateway = "0.0.0.0", ipv4_dns = [],
70 ipv6_address = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_prefix = "00",
71 ipv6_gateway = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_dns = [],
72 network_segment = "", hostname_resolution = []):
73 """
74 Creates a new interface object with the parameters provided and adds it to
75 the host selected.
76 If interface is successfuly created and the host exists, it returns the inteface id
77 It returns None otherwise
78 """
79 interface = model.api.newInterface(name, mac, ipv4_address, ipv4_mask, ipv4_gateway,
80 ipv4_dns, network_segment, hostname_resolution, parent_id=host_id)
81 if addInterface(host_id, interface):
82 return interface.getID()
83 return None
84
85
86 def createAndAddServiceToInterface(host_id, interface_id, name, protocol = "tcp?",
87 ports = [], status = "running", version = "unknown", description = ""):
88 service = model.api.newService(name, protocol, ports, status, version, description, parent_id=interface_id)
89 if addServiceToInterface(host_id, interface_id, service):
90 return service.getID()
91 return None
92
93
94 def createAndAddVulnToHost(host_id, name, desc, ref, severity="0",
95 resolution="", confirmed=True):
96 vuln = model.api.newVuln(name, desc, ref, severity, resolution,
97 confirmed=confirmed, parent_id=host_id)
98 if addVulnToHost(host_id, vuln):
99 return vuln.getID()
100 return None
101
102
103 def createAndAddVulnToInterface(host_id, interface_id, name, desc, ref,
104 severity="0", resolution="", confirmed=True):
105 vuln = model.api.newVuln(name, desc, ref, severity, resolution,
106 confirmed=confirmed, parent_id=interface_id)
107 if addVulnToInterface(host_id, interface_id, vuln):
108 return vuln.getID()
109 return None
110
111
112 def createAndAddVulnToService(host_id, service_id, name, desc, ref,
113 severity="0", resolution="", confirmed=True):
114 vuln = model.api.newVuln(name, desc, ref, severity, resolution,
115 confirmed=confirmed, parent_id=service_id)
116 if addVulnToService(host_id, service_id, vuln):
117 return vuln.getID()
118 return None
119
120
121 def createAndAddVulnWebToService(host_id, service_id, name, desc, website,
122 path, ref=None, severity="0", resolution="",
123 request=None, response=None, method=None,
124 pname=None, params=None, query=None,
125 category=None, confirmed=True):
126 vuln = model.api.newVulnWeb(name, desc, website, path, ref, severity,
127 resolution, request, response, method, pname,
128 params, query, category, confirmed=confirmed,
129 parent_id=service_id)
130 if addVulnToService(host_id, service_id, vuln):
131 return vuln.getID()
132 return None
133
134
135 def createAndAddVuln(model_object, name, desc, ref=None, severity="0",
136 resolution="", confirmed=True):
137 vuln = model.api.newVuln(name, desc, ref, severity, resolution,
138 confirmed=confirmed,
139 parent_id=model_object.getID())
140 if addVuln(model_object.getID(), vuln):
141 return vuln.getID()
142 return None
143
144
145 def createAndAddVulnWeb(model_object, name, desc, website, path, ref=None,
146 severity="0", resolution="", request=None,
147 response=None, method=None, pname=None, params=None,
148 query=None, category=None, confirmed=True):
149 vuln = model.api.newVulnWeb(name, desc, ref, severity, resolution, website,
150 path, request, response, method, pname, params,
151 query, category, confirmed=confirmed,
152 parent_id=model_object.getID())
153 if addVuln(model_object.getID(), vuln):
154 return vuln.getID()
155 return None
156
157
158 def createAndAddNoteToHost(host_id, name, text):
159 note = model.api.newNote(name, text, parent_id=host_id)
160 if addNoteToHost(host_id, note):
161 return note.getID()
162 return None
163
164
165 def createAndAddNoteToInterface(host_id, interface_id, name, text):
166 note = model.api.newNote(name, text, parent_id=interface_id)
167 if addNoteToInterface(host_id, interface_id, note):
168 return note.getID()
169 return None
170
171
172 def createAndAddNoteToService(host_id, service_id, name, text):
173 note = model.api.newNote(name, text, parent_id=service_id)
174 if addNoteToService(host_id, service_id, note):
175 return note.getID()
176 return None
177
178
179 def createAndAddNote(model_object, name, text):
180 note = model.api.newNote(name, text, parent_id=model_object.getID())
181 if addNote(model_object.getID(), note):
182 return note.getID()
183 return None
184
185
186 def createAndAddCred(model_object, username, password):
187 cred = model.api.newCred(username, password, parent_id=model_object.getID())
188 if addCred(model_object.getID(), cred):
189 return cred.getID()
190 return None
191
192
193 def createAndAddCredToHost(host_id, username, password):
194 cred = model.api.newCred(username, password, parent_id=host_id)
195 if addCredToHost(host_id, cred):
196 return cred.getID()
197 return None
198
199
200 def createAndAddCredToService(host_id, service_id, username, password):
201 cred = model.api.newCred(username, password, parent_id=service_id)
202 if addCredToService(host_id, service_id, cred):
203 return cred.getID()
204 return None
205
206
207 def addHost(host, category=None, update = False, old_hostname = None):
208 if host is not None:
209 __model_controller.addHostSYNC(host, category, update, old_hostname)
210 return True
211 return False
212
213 def addInterface(host_id, interface):
214 if interface is not None:
215 __model_controller.addInterfaceSYNC(host_id, interface)
216 return True
217 return False
218
219 def addApplication(host_id, application):
220 if application is not None:
221 __model_controller.addApplicationSYNC(host_id, application)
222 return True
223 return False
224
225 def addServiceToApplication(host_id, application_id, service):
226 if service is not None:
227 __model_controller.addServiceToApplicationSYNC(host_id, application_id, service)
228 return True
229 return False
230
231 def addServiceToInterface(host_id, interface_id, service):
232 if service is not None:
233 __model_controller.addServiceToInterfaceSYNC(host_id, interface_id, service)
234 return True
235 return False
236
237
238
239 def addVulnToHost(host_id, vuln):
240 if vuln is not None:
241 __model_controller.addVulnToHostSYNC(host_id, vuln)
242 return True
243 return False
244
245 def addVulnToInterface(host_id, interface_id, vuln):
246 if vuln is not None:
247 __model_controller.addVulnToInterfaceSYNC(host_id, interface_id, vuln)
248 return True
249 return False
250
251 def addVulnToApplication(host_id, application_id, vuln):
252 if vuln is not None:
253 __model_controller.addVulnToApplicationSYNC(host_id, application_id, vuln)
254 return True
255 return False
256
257 def addVulnToService(host_id, service_id, vuln):
258 if vuln is not None:
259 __model_controller.addVulnToServiceSYNC(host_id, service_id, vuln)
260 return True
261 return False
262
263 def addVuln(model_object_id, vuln):
264 if vuln is not None:
265 __model_controller.addVulnSYNC(model_object_id, vuln)
266 return True
267 return False
268
269
270
271 def addNoteToHost(host_id, note):
272 if note is not None:
273 __model_controller.addNoteToHostSYNC(host_id, note)
274 return True
275 return False
276
277 def addNoteToInterface(host_id, interface_id, note):
278 if note is not None:
279 __model_controller.addNoteToInterfaceSYNC(host_id, interface_id, note)
280 return True
281 return False
282
283 def addNoteToApplication(host_id, application_id, note):
284 if note is not None:
285 __model_controller.addNoteToApplicationSYNC(host_id, application_id, note)
286 return True
287 return False
288
289 def addNoteToService(host_id, service_id, note):
290 if note is not None:
291 __model_controller.addNoteToServiceSYNC(host_id, service_id, note)
292 return True
293 return False
294
295 def addNote(model_object_id, note):
296 if note is not None:
297 __model_controller.addNoteSYNC(model_object_id, note)
298 return True
299 return False
300
301
302 def addCred(model_object_id, cred):
303 if cred is not None:
304 __model_controller.addCredSYNC(model_object_id, cred)
305 return True
306 return False
307
308 def addCredToService(host_id, service_id, cred):
309 if cred is not None:
310 __model_controller.addCredToServiceSYNC(host_id, service_id, cred)
311 return True
312 return False
313
314 def addCredToHost(host_id, cred):
315 if cred is not None:
316 __model_controller.addCredToHostSYNC(host_id, cred)
317 return True
318 return False
319
320
321 def delHost(host_id):
322 __model_controller.delHostSYNC(host_id)
323 return True
324
325 def delApplication(host_id, application_id):
326 __model_controller.delApplicationSYNC(host_id, application_id)
327 return True
328
329 def delInterface(host_id, interface_id):
330 __model_controller.delInterfaceSYNC(host_id, interface_id)
331 return True
332
333 def delServiceFromHost(host_id, service_id):
334 __model_controller.delServiceFromHostSYNC(host_id, service_id)
335 return True
336
337 def delServiceFromInterface(host_id, interface_id, service_id):
338 __model_controller.delServiceFromInterfaceSYNC(host_id, interface_id, service_id)
339 return True
340
341 def delServiceFromApplication(host_id, application_id, service_id):
342 __model_controller.delServiceFromApplicationSYNC(host_id, application_id, service_id)
343 return True
344
345
346
347 def delVulnFromApplication(vuln, hostname, appname):
348 __model_controller.delVulnFromApplicationSYNC(hostname, appname, vuln)
349 return True
350
351 def delVulnFromInterface(vuln, hostname, intname):
352 __model_controller.delVulnFromInterfaceSYNC(hostname,intname, vuln)
353 return True
354
355 def delVulnFromHost(vuln, hostname):
356 __model_controller.delVulnFromHostSYNC(hostname,vuln)
357 return True
358
359
360 def delVulnFromService(vuln, hostname, srvname):
361 __model_controller.delVulnFromServiceSYNC(hostname,srvname, vuln)
362 return True
363
364 def delVuln(model_object_id, vuln_id):
365 __model_controller.delVulnSYNC(model_object_id, vuln_id)
366 return True
367
368
369
370 def delNoteFromApplication(note, hostname, appname):
371 __model_controller.delNoteFromApplicationSYNC(hostname, appname, note)
372 return True
373
374 def delNoteFromInterface(note, hostname, intname):
375 __model_controller.delNoteFromInterfaceSYNC(hostname,intname, note)
376 return True
377
378 def delNoteFromHost(note, hostname):
379 __model_controller.delNoteFromHostSYNC(hostname, note)
380 return True
381
382
383 def delNoteFromService(note, hostname, srvname):
384 __model_controller.delNoteFromServiceSYNC(hostname,srvname, note)
385 return True
386
387 def delNote(model_object_id, note_id):
388 __model_controller.delNoteSYNC(model_object_id, note_id)
389 return True
390
391
392 def delCred(model_object_id, cred_id):
393 __model_controller.delCredSYNC(model_object_id, cred_id)
394 return True
395
396 def delCredFromHost(cred, hostname):
397 __model_controller.delCredFromHostSYNC(hostname, cred)
398 return True
399
400
401 def delCredFromService(cred, hostname, srvname):
402 __model_controller.delCredFromServiceSYNC(hostname,srvname, cred)
403 return True
404
405
406
407
408
409 def editHost(host, name=None, description=None, os=None, owned=None):
410 __model_controller.editHostSYNC(host, name, description, os, owned)
411 return True
412
413 def editService(service, name=None, description=None, protocol=None, ports=None, status=None, version=None, owned=None):
414 __model_controller.editServiceSYNC(service, name, description, protocol, ports, status, version, owned)
415 return True
416
417 def editApplication(application, name, description, status, version, owned):
418 __model_controller.editApplicationSYNC(application, name, description, status, version, owned)
419 return True
420
421 def editInterface(interface, name=None, description=None, hostnames=None, mac=None, ipv4=None, ipv6=None, network_segment=None,
422 amount_ports_opened=None, amount_ports_closed=None, amount_ports_filtered=None, owned=None):
423 __model_controller.editInterfaceSYNC(interface, name, description, hostnames, mac, ipv4, ipv6, network_segment,
424 amount_ports_opened, amount_ports_closed, amount_ports_filtered, owned)
425 return True
426
427 def editNote(note, name=None, text=None):
428 __model_controller.editNoteSYNC(note, name, text)
429 return True
430
431 def editVuln(vuln, name=None, desc=None, severity=None, resolution=None, refs=None):
432 __model_controller.editVulnSYNC(vuln, name, desc, severity, resolution, refs)
433 return True
434
435 def editVulnWeb(vuln, name=None, desc=None, website=None, path=None, refs=None, severity=None, resolution=None,
436 request=None, response=None, method=None, pname=None, params=None, query=None, category=None):
437 __model_controller.editVulnWebSYNC(vuln, name, desc, website, path, refs, severity, resolution,
438 request, response, method, pname, params, query, category)
439 return True
440
441 def editCred(cred, username=None, password=None):
442 __model_controller.editCredSYNC(cred, username, password)
443 return True
444
445
446 def getParent(parent_id):
447 return __model_controller.find(parent_id)
448
449
450 def resolveConflicts():
451 __model_controller.resolveConflicts()
452
453 def resolveConflict(conflict, kwargs):
454 __model_controller.resolveConflict(conflict, kwargs)
455
456 def merge(host1, host2):
457 return __model_controller.merge(host1, host2)
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import threading
9 import logging
10 import logging.handlers
11 from gui.customevents import (LogCustomEvent,
12 ShowPopupCustomEvent,
13 ShowDialogCustomEvent)
14 #import qt
15 import model.guiapi
16
17 from config.configuration import getInstanceConfiguration
18 CONF = getInstanceConfiguration()
19
20 __the_logger = None
21 __notifier = None
22
23
24 def getLogger():
25 global __the_logger
26 if __the_logger is None:
27 # we create for the first time
28 __the_logger = AppLogger()
29 return __the_logger
30
31
32 def getNotifier(singleton=True):
33 global __notifier
34 if singleton:
35 if __notifier is None:
36 __notifier = Notifier()
37 return __notifier
38 else:
39 return Notifier()
40
41
42 class AppLogger(object):
43 """
44 a logging facility to show application activity
45 possible outputs are log file
46 console in a gui
47 This is thread safe. It uses an internal lock to make sure
48 text is logged sequentially
49 """
50
51 levels = {
52 "NOTIFICATION":logging.INFO,\
53 "INFO":logging.INFO,\
54 "WARNING":logging.WARNING,\
55 "ERROR":logging.ERROR,\
56 "CRITICAL":logging.CRITICAL,\
57 "DEBUG":logging.DEBUG \
58 }
59
60 def __init__(self, name = CONF.getAppname()):
61 # The logger instance.
62 self.__logger = logging.getLogger(name)
63
64 # We set the default level, this can be changed at any moment.
65 self.__logger.setLevel(logging.DEBUG)
66
67 # The name of the logger object.
68 self.__name = name
69
70 # The handler object.
71 self.__handler = None
72
73 # These flag will enable/disable the logger for different outputs
74 self.__output_file = False
75 self.__output_console = True
76
77 # a list of widgets we need to update when new text comes
78 self.__gui_output = []
79
80 self.__lock = threading.RLock()
81
82
83 def setLogFile(self, filename = None, maxsize=0,maxBackup=10, file_mode = 'a'):
84 """
85 Set a logfile as an output target. If no filename is given, then a
86 file with the instance name with the ".log" extension will be created.
87 The file rotates when the size is equal or higher than the
88 specified maxsize (in bytes) value. If the maxsize value is 0, the file will increase
89 indefinitely. The maxBackup value allows to create backups of rotated
90 files with the *.log.1, .2, etc. You can define the maximun value of
91 backup created files, if the value is zero, then no backup will be performed.
92 """
93
94 # Check if the filename is valid.
95
96 if filename is None:
97 # If not, then set a default name with the name of the instance.
98 self.__handler = logging.handlers.RotatingFileHandler('%s.log' % self.__name, 'a', maxsize,maxBackup)
99 else:
100 # If the file_mode is not allowed, open with 'append' mode
101 if not (file_mode == 'a' or file_mode == 'w'):
102 file_mode = 'a'
103
104 # The user must set a correct path and filename.
105 self.__handler = logging.handlers.RotatingFileHandler(filename, file_mode,maxsize,maxBackup)
106
107 # Set the standard formater to the handler. The '-8' parameter in the level name argument
108 # is a print format parameter, it means the converted value is left adjusted(-) and that
109 # it spans at max 8 positions(8).
110 self.__handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)-8s - %(message)s"))
111
112 # Don't forget to add the handler to the logger.
113 self.__logger.addHandler(self.__handler)
114
115 def isGUIOutputRegistered(self):
116 """
117 determines if there is at least one GUI widget registered
118 to show logger output
119 """
120 if self.__gui_output:
121 return True
122 else:
123 return False
124
125 def registerGUIOutput(self, widget):
126 """
127 adds a reference to a GUI widget we need to update
128 """
129 # IMPORTANT: the widget MUST implement a method called appendText
130 self.__gui_output.append(widget)
131
132 def clearWidgets(self):
133 self.__lock.acquire()
134 self.__gui_output = []
135 self.__lock.release()
136
137 def setLogLevel(self, level):
138 self.__logger.setLevel(level)
139
140 def enableLogOutput(self, enable=True):
141 self.__output_file = enable
142
143 def enableConsoleOutput(self, enable=True):
144 self.__output_console = enable
145
146 def getLogLevel(self):
147 """ Get the current log level. """
148 return self.__logger.getEffectiveLevel()
149
150 def isFileEnabled(self):
151 return self.__output_file
152
153 def isGUIEnabled(self):
154 return self.__output_console
155
156 def __notifyGUIConsole(self, msg):
157 """
158 notifies all GUI widgets registered
159 IMPORTANT: the widgets MUST be able to handle a custom event
160 """
161 for widget in self.__gui_output:
162 event = LogCustomEvent(msg)
163 #widget.update(event)
164 #qt.QApplication.postEvent(widget, event)
165 model.guiapi.postCustomEvent(event, widget)
166
167 def log(self, msg ,level = "INFO"):
168 """
169 Send a message to the logger with the specified level and format.
170 It will redirect the output to the current target.
171 The method will automatically filter those messages with a lower
172 "loglevel" than the specified.
173 It also will attempt to write the arguments by checking the format
174 list, if the arguments are incompatible with the format, it WON'T
175 log anything else than the message.
176 """
177 #TODO: we need to format the message to contain a timestamp for the
178 # gui output. The file handles this by itself, but for the gui, only a text
179 # message arrives
180 level = level.upper()
181 if level not in self.levels:
182 level = "INFO"
183
184 # take lock
185 self.__lock.acquire()
186 try:
187 if self.__handler and self.__output_file:
188 self.__logger.log(self.levels.get(level,logging.INFO), msg)
189 # Check if the log is being sent to the console
190 if self.__output_console:
191 self.__notifyGUIConsole("[%s] - %s" % (level, msg))
192 finally: # for safety so we don't block anything
193 # after doing all release
194 self.__lock.release()
195
196
197 class Notifier(object):
198 """
199 This class is used to show information to the user using dialog boxes or
200 little pop ups (like tooltips).
201 Also all notifications get logged using the Application Logger
202 """
203
204 #TODO: change the implementation to send/post custom events to avoid
205 # problems with threads like we had before
206 def __init__(self):
207 self.widget = None
208
209 def _postCustomEvent(self, text, level, customEventClass):
210 getLogger().log(text, "NOTIFICATION")
211 if self.widget is not None:
212 event = customEventClass(text, level)
213 #widget.update(event)
214 model.guiapi.postEvent(event, self.widget)
215
216 def showDialog(self, text, level="Information"):
217 self._postCustomEvent(text, level, ShowDialogCustomEvent)
218
219 def showPopup(self, text, level="Information"):
220 self._postCustomEvent(text, level, ShowPopupCustomEvent)
221
222
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 from gui.customevents import (ShowPopupCustomEvent,
9 ShowDialogCustomEvent)
10 import model.guiapi
11 from utils.logs import getLogger
12
13 from config.configuration import getInstanceConfiguration
14 CONF = getInstanceConfiguration()
15
16 __notifier = None
17
18
19 def getNotifier(singleton=True):
20 global __notifier
21 if singleton:
22 if __notifier is None:
23 __notifier = Notifier()
24 return __notifier
25 else:
26 return Notifier()
27
28
29 class Notifier(object):
30 """
31 This class is used to show information to the user using dialog boxes or
32 little pop ups (like tooltips).
33 Also all notifications get logged using the Application Logger
34 """
35
36 # TODO: change the implementation to send/post custom events to avoid
37 # problems with threads like we had before
38 def __init__(self):
39 self.widget = None
40
41 def _postCustomEvent(self, text, level, customEventClass):
42 getLogger().log(text, "INFO")
43 if self.widget is not None:
44 event = customEventClass(text, level)
45 model.guiapi.postEvent(event, self.widget)
46
47 def showDialog(self, text, level="Information"):
48 self._postCustomEvent(text, level, ShowDialogCustomEvent)
49
50 def showPopup(self, text, level="Information"):
51 self._postCustomEvent(text, level, ShowPopupCustomEvent)
225225 "severity": vuln.getSeverity(),
226226 "resolution": vuln.getResolution(),
227227 "refs": vuln.getRefs(),
228 "data": vuln.getData()
228 "data": vuln.getData(),
229 "confirmed": vuln.getConfirmed()
229230 })
230231 return doc
231232
235236 vuln.setResolution(doc.get("resolution"))
236237 vuln.setRefs(doc.get("refs"))
237238 vuln.setData(doc.get("data", ""))
239 vuln.setConfirmed(doc.get("confirmed", True))
238240 super(VulnMapper, self).unserialize(vuln, doc)
239241 return vuln
240242
9090 CADDVULNWEBSRV = 2038
9191 ADDNOTENOTE = 2039
9292 CADDNOTENOTE = 2039
93
93
9494 __descriptions = {
9595 ADDHOST : "ADDHOST",
9696 CADDHOST : "CADDHOST",
276276 modelactions.ADDNOTESRV : model.api.addNoteToService,
277277 modelactions.CADDNOTESRV : model.api.createAndAddNoteToService,
278278 modelactions.ADDNOTENOTE : model.api.addNoteToNote,
279 modelactions.CADDNOTENOTE : model.api.createAndAddNoteToNote,
279 modelactions.CADDNOTENOTE : model.api.createAndAddNoteToNote,
280280 #Creds
281281 modelactions.CADDCREDSRV : model.api.createAndAddCredToService,
282282 modelactions.ADDCREDSRV : model.api.addCredToService,
520520 def get_custom_file_path(self):
521521 return self._output_file_path
522522
523 def get_ws(self):
524 return CONF.getLastWorkspace()
525
523526 def getSettings(self):
524527 for param, (param_type, value) in self._settings.iteritems():
525528 yield param, value
543546 """
544547 return self._command_regex is not None and\
545548 self._command_regex.match(current_input.strip()) is not None
546
547
549
550
548551 def getCompletitionSuggestionsList(self, current_input):
549552 """
550553 This method can be overriden in the plugin implementation
551554 if a different kind of check is needed
552555 """
553
556
554557 words=current_input.split(" ")
555
556 cword=words[len(words)-1]
557
558
559
558
559 cword=words[len(words)-1]
560
561
562
560563 options={}
561564 for k,v in self._completition.iteritems():
562565 if re.search(str("^"+cword),k,flags=re.IGNORECASE):
563
566
564567 options[k]=v
565
568
566569 return options
567570
568571 def parseOutputString(self, output):
579582 With this method a plugin can add aditional arguments to the command that
580583 it's going to be executed.
581584 """
582 return None
585 return None
583586
584587 def getParsedElementsDict(self):
585588 """
608611 pass
609612
610613 def _set_host(self):
611
614
612615 pass
613
616
614617 def __addPendingAction(self, *args):
615618 """
616619 Adds a new pending action to the queue
630633 ipv6_address = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_prefix = "00",
631634 ipv6_gateway = "0000:0000:0000:0000:0000:0000:0000:0000", ipv6_dns = [],
632635 network_segment = "", hostname_resolution = []):
633 self.__addPendingAction(modelactions.CADDINTERFACE, host_id, name, mac, ipv4_address,
636 self.__addPendingAction(modelactions.CADDINTERFACE, host_id, name, mac, ipv4_address,
634637 ipv4_mask, ipv4_gateway, ipv4_dns, ipv6_address, ipv6_prefix, ipv6_gateway, ipv6_dns,
635638 network_segment, hostname_resolution)
636639 return factory.generateID(
642645 network_segment=network_segment,
643646 hostname_resolution=hostname_resolution)
644647
645 def createAndAddServiceToInterface(self, host_id, interface_id, name, protocol = "tcp?",
648 def createAndAddServiceToInterface(self, host_id, interface_id, name, protocol = "tcp?",
646649 ports = [], status = "running", version = "unknown", description = ""):
647 self.__addPendingAction(modelactions.CADDSERVICEINT, host_id, interface_id, name, protocol,
650 self.__addPendingAction(modelactions.CADDSERVICEINT, host_id, interface_id, name, protocol,
648651 ports, status, version, description)
649652 return factory.generateID(
650653 Service.class_signature,
680683 method, pname, params, query,category)
681684 return factory.generateID(
682685 ModelObjectVulnWeb.class_signature,
683 name=name, desc=desc, ref=ref, severity=severity, resolution=resolution,
686 name=name, desc=desc, ref=ref, severity=severity, resolution=resolution,
684687 website=website, path=path, request=request, response=response,
685688 method=method, pname=pname, params=params, query=query,
686689 category=category, parent_id=service_id)
744747
745748 def addVulnWebToService(self, host_id, service_id, vuln):
746749 self.__addPendingAction(modelactions.ADDVULNWEBSRV, host_id, service_id, vuln)
747
750
748751 def addNoteToHost(self, host_id, note):
749752 self.__addPendingAction(modelactions.ADDNOTEHOST, host_id, note)
750753
759762
760763 def addNoteToNote(self, host_id, service_id,note_id, note):
761764 self.__addPendingAction(modelactions.ADDNOTENOTE, host_id, service_id, note_id, note)
762
765
763766 def addCredToService(self, host_id, service_id, cred):
764767 self.__addPendingAction(modelactions.ADDCREDSRV, host_id, service_id, cred)
765768
766769 def delServiceFromInterface(self, service, hostname,
767770 intname, remote = True):
768771 self.__addPendingAction(modelactions.DELSERVICEINT,hostname,intname,service,remote)
769
772
770773 def log(self, msg, level='INFO'):
771774 self.__addPendingAction(modelactions.LOG,msg,level)
772775
773 def devlog(self, msg):
776 def devlog(self, msg):
774777 self.__addPendingAction(modelactions.DEVLOG,msg)
775778
776779
780783 self.output_queue = output_queue
781784 self.new_elem_queue = new_elem_queue
782785 self.plugin = plugin_instance
783
786
784787
785788 def run(self):
786789 proc_name = self.name
799802 try:
800803 self.output = output
801804 self.plugin.parseOutputString(output)
802 except Exception as e:
803 print ('Plugin Error: %s, (%s)' % (plugin.id, sha1OfStr(output)))
805 except Exception:
804806 model.api.log('Plugin Error: %s, (%s)' % (plugin.id, sha1OfStr(output)),"DEBUG")
805807 model.api.devlog("Plugin raised an exception:")
806808 model.api.devlog(traceback.format_exc())
819821 break
820822
821823 else:
822
824
823825 done = True
824826 model.api.devlog('%s: Exiting' % proc_name)
825 model.api.log('Plugin finished: %s, (%s)' % (plugin.id, sha1OfStr(self.output)),"DEBUG")
826 print ('Plugin finished: %s, (%s)' % (plugin.id, sha1OfStr(self.output)))
827
827 model.api.log('Plugin finished: %s, (%s)' % (plugin.id, sha1OfStr(self.output)))
828
828829 self.output_queue.task_done()
829830 self.new_elem_queue.put(None)
830831 return
3131 core.PluginBase.__init__(self)
3232 self.id = "Amap"
3333 self.name = "Amap Output Plugin"
34 self.plugin_version = "0.0.2"
34 self.plugin_version = "0.0.3"
3535 self.version = "5.4"
3636 self.options = None
3737 self._current_output = None
126126 service = services.get(key)
127127 s_id = self.createAndAddServiceToInterface(h_id, i_id, service[5], service[2], ports = [service[1]], status = service[3],description = service[6])
128128
129 if not debug:
130 os.remove(self._file_output_path)
131129 return True
132130
133131 file_arg_re = re.compile(r"^.*(-o \s*[^\s]+\s+(?:-m|)).*$")
178176 parser.add_argument('-m')
179177
180178
181
182 self._file_output_path=os.path.join(self.data_path,"amap_output-%s.txt" % random.uniform(1,10))
179 self._output_file_path = os.path.join(self.data_path,"%s_%s_output-%s.xml" % (self.get_ws(),
180 self.id,
181 random.uniform(1,10)))
183182
184183 if arg_match is None:
185184 final= re.sub(r"(^.*?amap)",
0 """
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4 """
0 # -*- coding: utf-8 -*-
1 """
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5 """
6 import re
7
8 from plugins import core
9
10
11 __author__ = u"Andres Tarantini"
12 __copyright__ = u"Copyright (c) 2015 Andres Tarantini"
13 __credits__ = [u"Andres Tarantini"]
14 __license__ = u"MIT"
15 __version__ = u"0.0.1"
16 __maintainer__ = u"Andres Tarantini"
17 __email__ = u"[email protected]"
18 __status__ = u"Development"
19
20
21 class DigPlugin(core.PluginBase):
22 """
23 Handle DiG (http://linux.die.net/man/1/dig) output
24 """
25 def __init__(self):
26 core.PluginBase.__init__(self)
27 self.id = u"dig"
28 self.name = u"DiG"
29 self.plugin_version = u"0.0.1"
30 self.version = u"9.9.5-3"
31 self._command_regex = re.compile(r'^(dig).*?')
32
33 def parseOutputString(self, output):
34 # Ignore all lines that start with ";"
35 parsed_output = [line for line in output.splitlines() if line and line[0] != u";"]
36 if not parsed_output:
37 return True
38
39 # Parse results
40 results = []
41 answer_section_columns = [u"domain", u"ttl", u"class", u"type", u"address"]
42 for line in parsed_output:
43 results.append(dict(zip(answer_section_columns, line.split())))
44
45 # Create hosts is results information is relevant
46 for result in results:
47 relevant_types = [u"A", u"AAAA"]
48 if result.get(u"type") in relevant_types:
49 ip_address = result.get(u"address")
50 domain = result.get(u"domain")
51
52 # Create host
53 host = self.createAndAddHost(ip_address)
54
55 # Create interface
56 if len(ip_address.split(".")) == 4:
57 iface = self.createAndAddInterface(
58 host,
59 ip_address,
60 ipv4_address=ip_address,
61 hostname_resolution=[domain]
62 )
63 else:
64 iface = self.createAndAddInterface(
65 host,
66 ip_address,
67 ipv6_address=ip_address,
68 hostname_resolution=[domain]
69 )
70
71 return True
72
73
74 def createPlugin():
75 return DigPlugin()
1111 import re
1212 import os
1313 import sys
14 import random
1415
1516
1617 current_path = os.path.abspath(os.getcwd())
5859 core.PluginBase.__init__(self)
5960 self.id = "Dnsmap"
6061 self.name = "Dnsmap XML Output Plugin"
61 self.plugin_version = "0.0.1"
62 self.plugin_version = "0.0.2"
6263 self.version = "0.30"
6364 self._completition = {
6465 "":"dnsmap &lt;target-domain&gt; [options]",
7576 self._command_regex = re.compile(r'^(sudo dnsmap|dnsmap|\.\/dnsmap).*?')
7677
7778 global current_path
78 self._output_file_path = os.path.join(self.data_path, "dnsmap_output-%s.txt" % self._rid)
79 self._output_file_path = os.path.join(self.data_path,"%s_%s_output-%s.xml" % (self.get_ws(),
80 self.id,
81 random.uniform(1,10)))
7982
8083 def canParseCommandString(self, current_input):
8184 if self._command_regex.match(current_input.strip()):
108111
109112 del parser
110113
111 if not debug:
112 os.remove(self._output_file_path)
113114 return True
114115
115116 xml_arg_re = re.compile(r"^.*(-c\s*[^\s]+).*$")
8686 for child in list(node):
8787 ret += self.parse_html_type(child)
8888 else:
89 ret += str(node.text).strip()
89 ret += node.text.encode("ascii", errors="backslashreplace").strip() if node.get('text') else ""
9090 if tag == 'listitem':
9191 if len(list(node)) > 0:
9292 for child in list(node):
9393 ret += self.parse_html_type(child)
9494 else:
95 ret = str(node.text).strip()
95 ret = node.text.encode("ascii", errors="backslashreplace").strip() if node.get('text') else ""
9696 if tag == 'orderedlist':
9797 i = 1
9898 for item in list(node):
103103 for child in list(node):
104104 ret += self.parse_html_type(child)
105105 else:
106 ret += str(node.text).strip()
106 ret += node.text.encode("ascii", errors="backslashreplace") if node.get('text') else ""
107107 if tag == 'unorderedlist':
108108 for item in list(node):
109109 ret += "\t" + "* " + self.parse_html_type(item) + "\n"
110110 if tag == 'urllink':
111 if node.text:
112 ret += str(node.text).strip() + " "
111 if node.get('text'):
112 ret += node.text.encode("ascii", errors="backslashreplace").strip() + " "
113113 last = ""
114114 for attr in node.attrib:
115 if node.get(attr) != node.get(last):
116 ret += str(node.get(attr)) + " "
115 if node.get(attr) and node.get(attr) != node.get(last):
116 ret += node.get(attr).encode("ascii", errors="backslashreplace") + " "
117117 last = attr
118118
119119 return ret
163163 vuln['desc'] += self.parse_html_type(htmlType)
164164 if item.tag == 'exploits':
165165 for exploit in list(item):
166 vuln['refs'].append(str(exploit.get('title')).strip() + ' ' + str(exploit.get('link')).strip())
166 if exploit.get('title') and exploit.get('link'):
167 title = exploit.get('title').encode("ascii", errors="backslashreplace").strip()
168 link = exploit.get('link').encode("ascii", errors="backslashreplace").strip()
169 vuln['refs'].append(title + ' ' + link)
167170 if item.tag == 'references':
168171 for ref in list(item):
169 vuln['refs'].append(str(ref.text).strip())
172 if ref.get('text'):
173 rf = ref.get('text').encode("ascii", errors="backslashreplace").strip()
174 vuln['refs'].append(rf)
170175 if item.tag == 'solution':
171176 for htmlType in list(item):
172177 vuln['resolution'] += self.parse_html_type(htmlType)
1313 import os
1414 import pprint
1515 import sys
16 import random
1617
1718 try:
1819 import xml.etree.cElementTree as ET
301302 Adds the -oX parameter to get xml output to the command string that the
302303 user has set.
303304 """
305 self._output_file_path = os.path.join(self.data_path,"%s_%s_output-%s.xml" % (self.get_ws(),
306 self.id,
307 random.uniform(1,10)))
304308 arg_match = self.xml_arg_re.match(command_string)
305309
306310
373373 core.PluginBase.__init__(self)
374374 self.id = "Nmap"
375375 self.name = "Nmap XML Output Plugin"
376 self.plugin_version = "0.0.2"
376 self.plugin_version = "0.0.3"
377377 self.version = "6.40"
378378 self.framework_version = "1.0.0"
379379 self.options = None
573573 Adds the -oX parameter to get xml output to the command string that the
574574 user has set.
575575 """
576 self._output_file_path = os.path.join(self.data_path,"nmap_output-%s.xml" % random.uniform(1,10))
576 self._output_file_path = os.path.join(self.data_path,"%s_%s_output-%s.xml" % (self.get_ws(),
577 self.id,
578 random.uniform(1,10)))
577579
578580 arg_match = self.xml_arg_re.match(command_string)
579581
1010 import re
1111 import os
1212 import sys
13 import random
1314
1415 try:
1516 import xml.etree.cElementTree as ET
6869 core.PluginBase.__init__(self)
6970 self.id = "Sslcheck"
7071 self.name = "Sslcheck XML Output Plugin"
71 self.plugin_version = "0.0.1"
72 self.plugin_version = "0.0.2"
7273 self.version = "0.30"
7374 self._completition = {
7475 "":"ssl-check [-h] [-r {key,ren,sign,serv,cyph,forw,heart,crime,all} [{key,ren,sign,serv,cyph,forw,heart,crime,all} ...]] -host HOST [-port PORT] [--xml] [--version]",
8687 self._command_regex = re.compile(r'^(sudo sslcheck|sslcheck|\.\/sslcheck).*?')
8788
8889 global current_path
89 self._output_file_path = os.path.join(self.data_path, "sslcheck_output-%s.xml" % self._rid)
90 self._output_file_path = os.path.join(self.data_path,"%s_%s_output-%s.xml" % (self.get_ws(),
91 self.id,
92 random.uniform(1,10)))
9093
9194 def canParseCommandString(self, current_input):
9295 if self._command_regex.match(current_input.strip()):
158161
159162 del parser
160163
161 if not debug:
162 os.remove(self._output_file_path)
163164 return True
164165
165166 xml_arg_re = re.compile(r"^.*(--xml\s*[^\s]+).*$")
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2
3 from plugins import core
4 import re
5
6 __author__ = "Ezequiel Tavella - @EzequielTBH"
7 __copyright__ = "Copyright 2015, @EzequielTBH"
8 __credits__ = "Ezequiel Tavella - @EzequielTBH"
9 __license__ = "GPL v3"
10 __version__ = "1.0.0"
11
12 class traceroutePlugin(core.PluginBase):
13
14 def __init__(self):
15 core.PluginBase.__init__(self)
16 self.id = "Traceroute"
17 self.name = "Traceroute"
18 self.plugin_version = "1.0.0"
19 self.command_string = ""
20 self._command_regex = re.compile(
21 r'^(traceroute|traceroute6).*?')
22
23
24 def parseOutputString(self, output, debug = False):
25
26 print("[*]Parsing Output...")
27
28 #Check no results.
29 if output.startswith("traceroute to") == False:
30 return
31
32 #Check if last parameter is host or ( packetlen or data size).
33 parameters = self.command_string.split(' ')
34 parameters.reverse()
35 hostName = parameters[0]
36
37 try:
38 int (hostName)
39 #No exception => host is the next item.
40 hostName = parameters[1]
41 except:
42 pass
43
44 #Add host and note with output of traceroute.
45 hostId = self.createAndAddHost(hostName)
46 self.createAndAddNoteToHost(hostId,"Traceroute Results", output)
47
48 print("[*]Parse finished, API faraday called...")
49
50 def processCommandString(self, username, current_path, command_string):
51
52 print("[*]traceroute Plugin running...")
53 self.command_string = command_string
54 return command_string
55
56 def createPlugin():
57 return traceroutePlugin()
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2
3 '''
4 Faraday Penetration Test IDE
5 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
6 See the file 'doc/LICENSE' for the license information
7 '''
8 from __future__ import with_statement
9 from plugins import core
10 import re
11 import os
12 import sys
13
14 try:
15 import xml.etree.cElementTree as ET
16 import xml.etree.ElementTree as ET_ORIG
17 ETREE_VERSION = ET_ORIG.VERSION
18 except ImportError:
19 import xml.etree.ElementTree as ET
20 ETREE_VERSION = ET.VERSION
21
22 ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")]
23
24 current_path = os.path.abspath(os.getcwd())
25
26 __author__ = "Morgan Lemarechal"
27 __copyright__ = "Copyright 2014, Faraday Project"
28 __credits__ = ["Morgan Lemarechal"]
29 __license__ = ""
30 __version__ = "1.0.0"
31 __maintainer__ = "Morgan Lemarechal"
32 __email__ = "[email protected]"
33 __status__ = "Development"
34
35
36
37
38
39 class WcscanParser(object):
40 """
41 The objective of this class is to parse an xml file generated by the wcscan tool.
42 TODO: Handle errors.
43 TODO: Test wcscan output version. Handle what happens if the parser doesn't support it.
44 TODO: Test cases.
45 @param wcscan_filepath A proper simple report generated by wcscan
46 """
47 def __init__(self, output):
48 self.scaninfo = {}
49 self.result = {}
50 tree = ET.parse(output)
51 root = tree.getroot()
52 for scan in root.findall(".//scan"):
53 infos = {}
54 for info in scan.attrib:
55 infos[info] = scan.attrib[info]
56 self.scaninfo[scan.attrib['file']] = infos
57
58 item = {}
59 if scan.attrib['type'] == "phpini":
60 for carac in scan:
61 item[carac.tag] = [carac.text,carac.attrib['rec'],""]
62
63 if scan.attrib['type'] == "webconfig":
64 id = 0
65 for carac in scan:
66 id += 1
67 item[id] = [carac.text,carac.attrib['rec'],carac.attrib['option'],carac.tag]
68
69 self.result[scan.attrib['file']] = item
70
71 class WcscanPlugin(core.PluginBase):
72 """
73 Example plugin to parse wcscan output.
74 """
75 def __init__(self):
76 core.PluginBase.__init__(self)
77 self.id = "Wcscan"
78 self.name = "Wcscan XML Output Plugin"
79 self.plugin_version = "0.0.1"
80 self.version = "0.30"
81 self._completition = {
82 "":"wcscan [-h] [-r] [-host HOST] [-port PORT] [--xml XMLOUTPUT] [--version] files [files ...]",
83 "-h":"show this help message and exit",
84 "-r":"enable the recommendation mode",
85 "--host":"to give the IP address of the conf file owner",
86 "--port":"to give a associated port",
87 "--xml":"enabled the XML output in a specified file",
88 "--version":"Show program's version number and exit",
89 }
90
91 self.options = None
92 self._current_output = None
93 self.current_path = None
94 self._command_regex = re.compile(r'^(sudo wcscan|wcscan|\.\/wcscan).*?')
95
96 global current_path
97 self._output_file_path = os.path.join(self.data_path, "wcscan_output-%s.xml" % self._rid)
98
99 def canParseCommandString(self, current_input):
100 if self._command_regex.match(current_input.strip()):
101 return True
102 else:
103 return False
104
105 def parseOutputString(self, output, debug=False):
106 """
107 This method will discard the output the shell sends, it will read it from
108 the xml where it expects it to be present.
109 NOTE: if 'debug' is true then it is being run from a test case and the
110 output being sent is valid.
111 """
112 if debug:
113 parser = WcscanParser(self._output_file_path)
114 else:
115
116 if not os.path.exists(self._output_file_path):
117 return False
118 parser = WcscanParser(self._output_file_path)
119
120 for file in parser.scaninfo:
121 host = parser.scaninfo[file]['host']
122 port = parser.scaninfo[file]['port']
123 h_id = self.createAndAddHost(host)
124 if(re.match("(^[2][0-5][0-5]|^[1]{0,1}[0-9]{1,2})\.([0-2][0-5][0-5]|[1]{0,1}[0-9]{1,2})\.([0-2][0-5][0-5]|[1]{0,1}[0-9]{1,2})\.([0-2][0-5][0-5]|[1]{0,1}[0-9]{1,2})$"
125 ,host)):
126 i_id = self.createAndAddInterface(h_id,
127 host,
128 ipv4_address = host)
129 else:
130 i_id = self.createAndAddInterface(h_id,
131 host,
132 ipv6_address = host)
133
134 s_id = self.createAndAddServiceToInterface(h_id, i_id, "http", protocol="tcp", ports=port)
135 for vuln in parser.result[file]:
136 if parser.scaninfo[file]['type'] == "phpini":
137 v_id = self.createAndAddVulnToService(h_id, s_id,
138 parser.scaninfo[file]['file']+":"+vuln,
139 desc="{} : {}\n{}".format(vuln,
140 str(parser.result[file][vuln][0]),
141 str(parser.result[file][vuln][1])),
142 severity=0)
143
144 if parser.scaninfo[file]['type'] == "webconfig":
145 v_id = self.createAndAddVulnToService(h_id, s_id,
146 parser.scaninfo[file]['file']+":"+str(parser.result[file][vuln][3]),
147 desc="{} : {} = {}\n{}".format(str(parser.result[file][vuln][3]),
148 str(parser.result[file][vuln][2]),
149 str(parser.result[file][vuln][0]),
150 str(parser.result[file][vuln][1])),
151 severity=0)
152 del parser
153
154 if not debug:
155 os.remove(self._output_file_path)
156 return True
157
158 xml_arg_re = re.compile(r"^.*(--xml\s*[^\s]+).*$")
159
160 def processCommandString(self, username, current_path, command_string):
161 """
162 Adds the parameter to get output to the command string that the
163 user has set.
164 """
165
166 arg_match = self.xml_arg_re.match(command_string)
167
168 if arg_match is None:
169 return "%s --xml %s" % (command_string, self._output_file_path)
170 else:
171 return re.sub(arg_match.group(1),
172 r"-xml %s" % self._output_file_path,
173 command_string)
174
175 def createPlugin():
176 return WcscanPlugin()
177
178 if __name__ == '__main__':
179 parser = WcscanParser(sys.argv[1])
180 for item in parser.items:
181 if item.status == 'up':
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2
3 '''
4 Faraday Penetration Test IDE
5 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
6 See the file 'doc/LICENSE' for the license information
7 '''
8 from __future__ import with_statement
9 from plugins import core
10 import re
11 import os
12 import sys
13 import random
14
15 try:
16 import xml.etree.cElementTree as ET
17 import xml.etree.ElementTree as ET_ORIG
18 ETREE_VERSION = ET_ORIG.VERSION
19 except ImportError:
20 import xml.etree.ElementTree as ET
21 ETREE_VERSION = ET.VERSION
22
23 ETREE_VERSION = [int(i) for i in ETREE_VERSION.split(".")]
24
25 current_path = os.path.abspath(os.getcwd())
26
27 __author__ = "Morgan Lemarechal"
28 __copyright__ = "Copyright 2014, Faraday Project"
29 __credits__ = ["Morgan Lemarechal"]
30 __license__ = ""
31 __version__ = "1.0.0"
32 __maintainer__ = "Morgan Lemarechal"
33 __email__ = "[email protected]"
34 __status__ = "Development"
35
36
37
38
39
40 class WcscanParser(object):
41 """
42 The objective of this class is to parse an xml file generated by the wcscan tool.
43 TODO: Handle errors.
44 TODO: Test wcscan output version. Handle what happens if the parser doesn't support it.
45 TODO: Test cases.
46 @param wcscan_filepath A proper simple report generated by wcscan
47 """
48 def __init__(self, output):
49 self.scaninfo = {}
50 self.result = {}
51 tree = ET.parse(output)
52 root = tree.getroot()
53 for scan in root.findall(".//scan"):
54 infos = {}
55 for info in scan.attrib:
56 infos[info] = scan.attrib[info]
57 self.scaninfo[scan.attrib['file']] = infos
58
59 item = {}
60 if scan.attrib['type'] == "phpini":
61 for carac in scan:
62 item[carac.tag] = [carac.text,carac.attrib['rec'],""]
63
64 if scan.attrib['type'] == "webconfig":
65 id = 0
66 for carac in scan:
67 id += 1
68 item[id] = [carac.text,carac.attrib['rec'],carac.attrib['option'],carac.tag]
69
70 self.result[scan.attrib['file']] = item
71
72 class WcscanPlugin(core.PluginBase):
73 """
74 Example plugin to parse wcscan output.
75 """
76 def __init__(self):
77 core.PluginBase.__init__(self)
78 self.id = "Wcscan"
79 self.name = "Wcscan XML Output Plugin"
80 self.plugin_version = "0.0.2"
81 self.version = "0.30"
82 self._completition = {
83 "":"wcscan [-h] [-r] [-host HOST] [-port PORT] [--xml XMLOUTPUT] [--version] files [files ...]",
84 "-h":"show this help message and exit",
85 "-r":"enable the recommendation mode",
86 "--host":"to give the IP address of the conf file owner",
87 "--port":"to give a associated port",
88 "--xml":"enabled the XML output in a specified file",
89 "--version":"Show program's version number and exit",
90 }
91
92 self.options = None
93 self._current_output = None
94 self.current_path = None
95 self._command_regex = re.compile(r'^(sudo wcscan|wcscan|\.\/wcscan).*?')
96
97 global current_path
98 self._output_file_path = os.path.join(self.data_path,"%s_%s_output-%s.xml" % (self.get_ws(),
99 self.id,
100 random.uniform(1,10)))
101
102
103 def canParseCommandString(self, current_input):
104 if self._command_regex.match(current_input.strip()):
105 return True
106 else:
107 return False
108
109 def parseOutputString(self, output, debug=False):
110 """
111 This method will discard the output the shell sends, it will read it from
112 the xml where it expects it to be present.
113 NOTE: if 'debug' is true then it is being run from a test case and the
114 output being sent is valid.
115 """
116 if debug:
117 parser = WcscanParser(self._output_file_path)
118 else:
119
120 if not os.path.exists(self._output_file_path):
121 return False
122 parser = WcscanParser(self._output_file_path)
123
124 for file in parser.scaninfo:
125 host = parser.scaninfo[file]['host']
126 port = parser.scaninfo[file]['port']
127 h_id = self.createAndAddHost(host)
128 if(re.match("(^[2][0-5][0-5]|^[1]{0,1}[0-9]{1,2})\.([0-2][0-5][0-5]|[1]{0,1}[0-9]{1,2})\.([0-2][0-5][0-5]|[1]{0,1}[0-9]{1,2})\.([0-2][0-5][0-5]|[1]{0,1}[0-9]{1,2})$"
129 ,host)):
130 i_id = self.createAndAddInterface(h_id,
131 host,
132 ipv4_address = host)
133 else:
134 i_id = self.createAndAddInterface(h_id,
135 host,
136 ipv6_address = host)
137
138 s_id = self.createAndAddServiceToInterface(h_id, i_id, "http", protocol="tcp", ports=port)
139 for vuln in parser.result[file]:
140 if parser.scaninfo[file]['type'] == "phpini":
141 v_id = self.createAndAddVulnToService(h_id, s_id,
142 parser.scaninfo[file]['file']+":"+vuln,
143 desc="{} : {}\n{}".format(vuln,
144 str(parser.result[file][vuln][0]),
145 str(parser.result[file][vuln][1])),
146 severity=0)
147
148 if parser.scaninfo[file]['type'] == "webconfig":
149 v_id = self.createAndAddVulnToService(h_id, s_id,
150 parser.scaninfo[file]['file']+":"+str(parser.result[file][vuln][3]),
151 desc="{} : {} = {}\n{}".format(str(parser.result[file][vuln][3]),
152 str(parser.result[file][vuln][2]),
153 str(parser.result[file][vuln][0]),
154 str(parser.result[file][vuln][1])),
155 severity=0)
156 del parser
157
158 return True
159
160 xml_arg_re = re.compile(r"^.*(--xml\s*[^\s]+).*$")
161
162 def processCommandString(self, username, current_path, command_string):
163 """
164 Adds the parameter to get output to the command string that the
165 user has set.
166 """
167
168 arg_match = self.xml_arg_re.match(command_string)
169
170 if arg_match is None:
171 return "%s --xml %s" % (command_string, self._output_file_path)
172 else:
173 return re.sub(arg_match.group(1),
174 r"-xml %s" % self._output_file_path,
175 command_string)
176
177 def createPlugin():
178 return WcscanPlugin()
179
180 if __name__ == '__main__':
181 parser = WcscanParser(sys.argv[1])
182 for item in parser.items:
183 if item.status == 'up':
182184 print item
8585 core.PluginBase.__init__(self)
8686 self.id = "Webfuzzer"
8787 self.name = "Webfuzzer Output Plugin"
88 self.plugin_version = "0.0.1"
88 self.plugin_version = "0.0.2"
8989 self.version = "0.2.0"
9090 self.options = None
9191 self._current_output = None
148148 n2_id = self.createAndAddNoteToNote(h_id,s_id,n_id,parser.hostname,"")
149149
150150 del parser
151
152 if not debug:
153 os.remove(self._output_path)
151
154152 return True
155153
156154
44
55 '''
66
7 #!/usr/bin/env python
8 # -*- coding: utf-8 -*-
9
10 import re
11 from lxml import etree as ET
12
13 VRAI = ['yes','on','1']
14 FAUX = ['no','off','0']
15
16 def equals(value1,value2):
17 if (value1.lower() in VRAI and value2.lower() in VRAI) or (value1.lower() in FAUX and value2.lower() in FAUX):
18 return True
19 else:
20 return False
21
22 #Good practices
23 rules = {
24 'session.use_cookies':['Cookies for sessions are disabled','1'],
25 'session.use_only_cookies':['Only cookies for sessions is disabled','1'],
26 'session.cookie_httponly':['Cookies are not set to HTTP only','1'],
27 'session.bug_compat_42':['Bug compatibility 42 is enabled','0'],
28 'session.bug_compat_warn':['Bug compatibility 42 warning is enabled','0'],
29 'session.use_trans_sid':['Use of \'use_trans_sid\' is considered harmful','0'],
30 'session.cookie_secure':['Cookie is not set to secure connection','1'],
31 'session.use_strict_mode':['Strict mode is disabled for session fixation prevention','1'],
32 'session.cookie_domain':['Cookie domain is not set',''],
33 'session.hash_function':['Weak session id generation: a stronger hash function such as SHA256 should be used',''],
34 'allow_url_fopen':['Remote file opening is allowed','off'],
35 'allow_url_include':['Remote file including is allowed','off'],
36 'error_reporting':['Errors reports are enabled in production','0'],
37 'display_errors':['Errors should not be shown in production','off'],
38 'log_errors':['Log errors are not written in production','on'],
39 'expose_php':['PHP signature should disabled','off'],
40 'register_globals':['Register globals is enabled','off'],
41 'magic_quotes_gpc':['Magic quotes is enabled','off'],
42 'magic_quotes_runtime':['Magic quotes is enabled at runtime','off'],
43 'safe_mode':['Safe mode is enabled','off'],
44 'register_long_arrays':['Register long arrays is enabled','off'],
45 'display_startup_errors':['Startup errors is displayed','off'],
46 'max_input_vars':['Maximum input variables is not set',''],
47 'open_basedir':['You should restrict PHP\'s file system access (basedir)',''],
48 'memory_limit':['You should define a reasonable memory limit (<128 M)',''],
49 'post_max_size':['You should define a reasonable max post size ',''],
50 'upload_max_filesize':['You should define a reasonable max upload size',''],
51 'upload_tmp_dir':['You should define a temporary directory used for file uploads',''],
52 'asp_tags':['ASP tag handling is not turned off','0'],
53 'xdebug.default_enable':['Xdebug is enabled','0'],
54 'xdebug.remote_enable':['Xdebug should not be trying to contact debug clients','0'],
55 }
56
57 #Lines of config whose default values are bad
58 bad_default_config = ['session.cookie_httponly','session.bug_compat_42',
59 'session.bug_compat_warn','allow_url_fopen',
60 'error_reporting','display_errors','log_errors',
61 'expose_php','magic_quotes_gpc','register_long_arrays']
62
63 #Lines of config that is not set (value by the user)
64 is_set_config = ['max_input_vars','open_basedir', 'memory_limit','post_max_size',
65 'upload_max_filesize','upload_tmp_dir']
66
67 #Weak hashing functions
68 weak_functions = ['md2', 'md4', 'md5', 'sha1', 'gost', 'snefru', '0', '1']
69
70 #To-be-disabled functions
71 weak_php_functions = ['passthru', 'shell_exec', 'exec',
72 'system', 'popen', 'stream_select']
73
74
75 class PhpIniScan:
76
77 def __init__(self, file, xml):
78 self.config = ""
79 for line in open(file,"r"):
80 if not re.match(r'(^(;|\[))',line,) and line[0] != '\n':
81 self.config += line.lstrip()
82 self.recommended = "\nRecommended configuration changes in php.ini:\n"
83 self.xml = xml
84
85 def xml_export(self,directive,rec):
86 if self.xml is not None:
87 newElement = ET.SubElement(self.xml, directive[0])
88 newElement.attrib['rec'] = rec
89 newElement.text = directive[1]
90
91 def global_check(self):
92 print "[+]\033[0;41mVulnerabilites/Informations\033[0m:"
93 for line in self.config.split('\n'):
94 directive = ''.join(line.split()).split('=')
95
96 if ( rules.has_key(directive[0]) and not equals(rules[directive[0]][1],directive[1])) or directive[0] in bad_default_config:
97 print " \033[1;30m({})\033[0m {}".format(directive[0],rules[directive[0]][0])
98 self.recommended += " {} = {}\n".format(directive[0],rules[directive[0]][1])
99 self.xml_export(directive,rules[directive[0]][0])
100 continue
101
102 if rules.has_key(directive[0]) and directive[1] == "":
103 print " \033[1;30m({})\033[0m {}".format(directive[0],rules[directive[0]][0])
104 self.recommended += " {} = {}\n".format(directive[0],rules[directive[0]][1])
105 self.xml_export(directive,rules[directive[0]][0])
106 continue
107
108 if directive[0] == "session.hash_function" and directive[1] in weak_functions:
109 print " \033[1;30m({})\033[0m {}".format(directive[0],rules[directive[0]][0])
110 self.recommended += " {} = sha256\n".format(directive[0])
111 self.xml_export(directive,rules[directive[0]][0])
112 continue
113
114 if directive[0] == "disable_functions":
115 for option in weak_php_functions:
116 if not option in directive[1]:
117 print " \033[1;30m(disable_functions)\033[0m {} not listed".format(option)
118 self.recommended += " disable_functions = ... , {} , ...\n".format(option)
119 self.xml_export(directive,"")
120 continue
121
122 for element in is_set_config:
123 if not element in self.config:
124 print " \033[1;30m({})\033[0m {}".format(element,rules[element][0])
125 self.recommended += " {} is not set\n".format(element)
126 directive = [element,'isNotSet']
127 self.xml_export(directive,rules[directive[0]][0])
128
129 for element in bad_default_config:
130 if not element in self.config:
131 print " \033[1;30m({})\033[0m {}".format(element,rules[element][0])
132 self.recommended += " {} = {}\n".format(element,rules[element][1])
133 directive = [element,'defaultValue']
134 self.xml_export(directive,rules[directive[0]][0])
135
136 # TODO Session save path not set or world writeable || CheckSessionPath
137 # TODO Entropy file is not defined || CheckSessionEntropyPath
138 # TODO Maximum post size too large || MaximumPostSize
139 # TODO Disable harmful CLI functions || DisableCliFunctions
140 # TODO CVE-2013-1635 || CheckSoapWsdlCacheDir
141 # TODO Ensure file uploads workCheck || UploadTmpDir
142 # TODO check the sizes (in M) f some parameters)
143
144 def scanner(file,recmode,xml):
145 filetoscan = PhpIniScan(file,xml)
146 filetoscan.global_check()
147 if recmode:
7 #!/usr/bin/env python
8 # -*- coding: utf-8 -*-
9
10 import re
11 from lxml import etree as ET
12
13 VRAI = ['yes','on','1']
14 FAUX = ['no','off','0']
15
16 def equals(value1,value2):
17 if (value1.lower() in VRAI and value2.lower() in VRAI) or (value1.lower() in FAUX and value2.lower() in FAUX):
18 return True
19 else:
20 return False
21
22 #Good practices
23 rules = {
24 'session.use_cookies':['Cookies for sessions are disabled','1'],
25 'session.use_only_cookies':['Only cookies for sessions is disabled','1'],
26 'session.cookie_httponly':['Cookies are not set to HTTP only','1'],
27 'session.bug_compat_42':['Bug compatibility 42 is enabled','0'],
28 'session.bug_compat_warn':['Bug compatibility 42 warning is enabled','0'],
29 'session.use_trans_sid':['Use of \'use_trans_sid\' is considered harmful','0'],
30 'session.cookie_secure':['Cookie is not set to secure connection','1'],
31 'session.use_strict_mode':['Strict mode is disabled for session fixation prevention','1'],
32 'session.cookie_domain':['Cookie domain is not set',''],
33 'session.hash_function':['Weak session id generation: a stronger hash function such as SHA256 should be used',''],
34 'allow_url_fopen':['Remote file opening is allowed','off'],
35 'allow_url_include':['Remote file including is allowed','off'],
36 'error_reporting':['Errors reports are enabled in production','0'],
37 'display_errors':['Errors should not be shown in production','off'],
38 'log_errors':['Log errors are not written in production','on'],
39 'expose_php':['PHP signature should disabled','off'],
40 'register_globals':['Register globals is enabled','off'],
41 'magic_quotes_gpc':['Magic quotes is enabled','off'],
42 'magic_quotes_runtime':['Magic quotes is enabled at runtime','off'],
43 'safe_mode':['Safe mode is enabled','off'],
44 'register_long_arrays':['Register long arrays is enabled','off'],
45 'display_startup_errors':['Startup errors is displayed','off'],
46 'max_input_vars':['Maximum input variables is not set',''],
47 'open_basedir':['You should restrict PHP\'s file system access (basedir)',''],
48 'memory_limit':['You should define a reasonable memory limit (<128 M)',''],
49 'post_max_size':['You should define a reasonable max post size ',''],
50 'upload_max_filesize':['You should define a reasonable max upload size',''],
51 'upload_tmp_dir':['You should define a temporary directory used for file uploads',''],
52 'asp_tags':['ASP tag handling is not turned off','0'],
53 'xdebug.default_enable':['Xdebug is enabled','0'],
54 'xdebug.remote_enable':['Xdebug should not be trying to contact debug clients','0'],
55 }
56
57 #Lines of config whose default values are bad
58 bad_default_config = ['session.cookie_httponly','session.bug_compat_42',
59 'session.bug_compat_warn','allow_url_fopen',
60 'error_reporting','display_errors','log_errors',
61 'expose_php','magic_quotes_gpc','register_long_arrays']
62
63 #Lines of config that is not set (value by the user)
64 is_set_config = ['max_input_vars','open_basedir', 'memory_limit','post_max_size',
65 'upload_max_filesize','upload_tmp_dir']
66
67 #Weak hashing functions
68 weak_functions = ['md2', 'md4', 'md5', 'sha1', 'gost', 'snefru', '0', '1']
69
70 #To-be-disabled functions
71 weak_php_functions = ['passthru', 'shell_exec', 'exec',
72 'system', 'popen', 'stream_select']
73
74
75 class PhpIniScan:
76
77 def __init__(self, file, xml):
78 self.config = ""
79 for line in open(file,"r"):
80 if not re.match(r'(^(;|\[))',line,) and line[0] != '\n':
81 self.config += line.lstrip()
82 self.recommended = "\nRecommended configuration changes in php.ini:\n"
83 self.xml = xml
84
85 def xml_export(self,directive,rec):
86 if self.xml is not None:
87 newElement = ET.SubElement(self.xml, directive[0])
88 newElement.attrib['rec'] = rec
89 newElement.text = directive[1]
90
91 def global_check(self):
92 print "[+]\033[0;41mVulnerabilites/Informations\033[0m:"
93 for line in self.config.split('\n'):
94 directive = ''.join(line.split()).split('=')
95
96 if ( rules.has_key(directive[0]) and not equals(rules[directive[0]][1],directive[1])) or directive[0] in bad_default_config:
97 print " \033[1;30m({})\033[0m {}".format(directive[0],rules[directive[0]][0])
98 self.recommended += " {} = {}\n".format(directive[0],rules[directive[0]][1])
99 self.xml_export(directive,rules[directive[0]][0])
100 continue
101
102 if rules.has_key(directive[0]) and directive[1] == "":
103 print " \033[1;30m({})\033[0m {}".format(directive[0],rules[directive[0]][0])
104 self.recommended += " {} = {}\n".format(directive[0],rules[directive[0]][1])
105 self.xml_export(directive,rules[directive[0]][0])
106 continue
107
108 if directive[0] == "session.hash_function" and directive[1] in weak_functions:
109 print " \033[1;30m({})\033[0m {}".format(directive[0],rules[directive[0]][0])
110 self.recommended += " {} = sha256\n".format(directive[0])
111 self.xml_export(directive,rules[directive[0]][0])
112 continue
113
114 if directive[0] == "disable_functions":
115 for option in weak_php_functions:
116 if not option in directive[1]:
117 print " \033[1;30m(disable_functions)\033[0m {} not listed".format(option)
118 self.recommended += " disable_functions = ... , {} , ...\n".format(option)
119 self.xml_export(directive,"")
120 continue
121
122 for element in is_set_config:
123 if not element in self.config:
124 print " \033[1;30m({})\033[0m {}".format(element,rules[element][0])
125 self.recommended += " {} is not set\n".format(element)
126 directive = [element,'isNotSet']
127 self.xml_export(directive,rules[directive[0]][0])
128
129 for element in bad_default_config:
130 if not element in self.config:
131 print " \033[1;30m({})\033[0m {}".format(element,rules[element][0])
132 self.recommended += " {} = {}\n".format(element,rules[element][1])
133 directive = [element,'defaultValue']
134 self.xml_export(directive,rules[directive[0]][0])
135
136 # TODO Session save path not set or world writeable || CheckSessionPath
137 # TODO Entropy file is not defined || CheckSessionEntropyPath
138 # TODO Maximum post size too large || MaximumPostSize
139 # TODO Disable harmful CLI functions || DisableCliFunctions
140 # TODO CVE-2013-1635 || CheckSoapWsdlCacheDir
141 # TODO Ensure file uploads workCheck || UploadTmpDir
142 # TODO check the sizes (in M) f some parameters)
143
144 def scanner(file,recmode,xml):
145 filetoscan = PhpIniScan(file,xml)
146 filetoscan.global_check()
147 if recmode:
148148 print filetoscan.recommended
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import os
9 import pwd
10 import re
11 from cStringIO import StringIO
12
13 from model.common import ModelObject
14 from shell.core.history import HistoryTypeBuffer
15 import model.api as api
16
17 from ecma48 import strip_control_sequences
18 #TODO: check from config if it is qt3 or qt4 and import the right one
19 from gui.qt3.pyqonsole.widget import ShellWidget
20 from shell.controller.qt3.session import Session
21 #from shell.controller.qt3.procctrl import ProcessController
22 from model.common import TreeWordsTries
23 from config.configuration import getInstanceConfiguration
24 CONF = getInstanceConfiguration()
25
26 next_id = 1
27 #-------------------------------------------------------------------------------
28
29 #TODO: decide if we really need to inherit from ModelObject
30 class ShellEnvironment(ModelObject):
31 """
32 Shell environment is really a group of components that need to work together
33 The environment is composed by:
34 * a ShellWidget used in the GUI
35 * a Session (which is the real shell)
36 * a PluginController needed to handle all shell input and output
37 The shell environment interacts with the Model Controller to add new hosts
38 """
39 def __init__(self, name, qapp, gui_parent, model_controller,
40 plugin_controller, close_callback=None):
41
42 ModelObject.__init__(self)
43 self._id = self.get_id()
44 self.name = name
45
46 # a reference used to add new hosts
47 self._model_controller = model_controller
48
49 # create the widget
50 self.widget = ShellWidget(qapp,gui_parent,name)
51 self.widget.setupLayout()
52
53 # For safesty we don't use the user shell, we force it to bash
54 progname = "/bin/bash"
55 # create the session
56 #Session(gui, pgm, args, term, sessionid='session-1', cwd=None):
57 #self.process_controller = ProcessController()
58 self.session = Session(self.widget, progname, [], "xterm", name);
59
60 self.session.setConnect(True)
61 self.session.setHistory(HistoryTypeBuffer(1000))
62 self._setUpSessionSinalHandlers()
63
64 #self.__last_user_input = None
65 #self.__user_input_signal = False
66
67 # flag that determines if output has to be ignored
68 self.__ignore_process_output = False
69 self.__ignore_process_output_once = False
70 self.__first_process_output = True
71 self.__save_output_prompt_format = False
72
73 # determines if input is for an interactive command or not
74 self.__interactive = False
75
76 #TODO: check if we need to connect to this signal
77 self.session.myconnect('done', self.close)
78
79 # instance a new plugin controller
80 self.plugin_controller = plugin_controller(self.id)
81
82 self._initial_prompt = ""
83 #self._custom_prompt_format = re.compile("\\x1B\[1;32m\[.+@.+\s.+\]>(\$|#)\s+\\x1B\[m")
84 #self._custom_prompt_format = re.compile("\[.+@.+\s.+\]>(\$|#)\s+")
85 self._custom_prompt_format = re.compile("\[(?P<user>.+)@(?P<host>.+):(?P<path>.+)\]>(\$|#)")
86 #TODO: somewhere in the config there should be a list of regexes used to match
87 # prompts. Be careful with this because if regexes are too generic they will
88 # match more that is needed.
89 self._generic_prompt_formats = []
90 #XXX: adding this format just as a test! This should be in the config somehow
91 # Also this may be is too generic to use...
92 self._generic_prompt_formats.append(re.compile("^.+@.+:.+(\$|#)$"))
93
94 #This is a reference to MainApplication.deleteShellEnvironment
95 self._close_callback = close_callback
96
97 # flag that determines if shell environment is running
98 self.__running = False
99
100 #Autocomplete
101 self._options=[] #only keys
102 self._tname="" # last tool executed
103 self._lcount=0
104 self._optionsh={} #keys + help
105 self.__command = ''
106 def __del__(self):
107 """
108 When deleteing the environment we have to delete all the components
109 """
110 #print "ShellEnvironment __del__ called"
111 del self.session
112 #TODO: delete tab holding current ShellWidget
113 #tabmgr = self.widget.parentWidget()
114 #tabmgr.removeView(self.widget)
115 #FIXME: check the __del__ method in ShellWidget
116 #del self.widget
117 #TODO: make sure the plugin controller correctly finishes all processes
118 del self.plugin_controller
119
120 def close(self, session, status, *args):
121 #TODO: por alguna razon queda colgado un QSocketNotifier
122 # QSocketNotifier: invalid socket 17 and type 'Read', disabling...
123 # y eso cuelga la aplicacion
124 api.devlog("ShellEnvironment close was called - session = %r, status = %r , *args = %r" % (session, status, args))
125 if self._close_callback is not None:
126 self._close_callback(self.name, self)
127 else:
128 api.devlog("close was call but callback is not set")
129
130 #def setCloseCallback(self, ref):
131 # self._close_callback = ref
132
133 def get_id(self):
134 global next_id
135 # just to make sure each env has a differente id because names can be the same
136 id = next_id
137 next_id += 1
138 return id
139
140 def run(self):
141 self.session.run()
142 self.__running = True
143 #self.plugin_controller.run()
144
145 def _setUpSessionSinalHandlers(self):
146 """
147 Connects some signal handlers to different signals emmited inside shell
148 emulation and pty.
149 These signals are used to handle user input and child process output
150 """
151 #IMPORTANT ABOUT USER INPUT AND PROCESS OUPUT (Session class)
152 #PtyProcess in method dataReceived gets the child output
153 #in method sendBytes it has the user input
154 #
155 #Emulation in method sendString also has the user input
156 #also check method onKeyPressed to identify when an ENTER key is pressed
157 #
158 #
159 #OUTPUT
160 # emulation onRcvBlock le llega la salida del proceso
161 # self.myconnect('receivedStdout', self.dataReceived)
162 # Este se llama desde childOutput en pty_
163 # self.myemit("receivedStdout", (fdno, lenlist))
164 #
165 # self.myemit('block_in', (buf,)) <--- se llama desde dataReceived
166 # self.sh.myconnect('block_in', self.em.onRcvBlock)
167 #
168 #INPUT
169 # self.myemit("sndBlock", (string,))
170 # self.em.myconnect('sndBlock', self.sh.sendBytes)
171 #
172 # otra opcion es agregar una senal propia en los metodos sendString
173 # o en onKeyPressed de emuVt102
174
175 # connect signals to handle shell I/O
176 self.session.sh.myconnect('processOutput', self.processOutputHandler)
177
178 #XXX: nasty hack to be able to send a return value
179 # Using myconnect and emitting singals won't allow us to return a value
180 self.session.em.sendENTER = self.processUserInputBuffer
181 self.widget.myconnect('ignoreShellWidgetResize', self.ignoreDueResize)
182
183 #handle ctrl+space
184 self.session.em.sendCTRLSPACE = self.processCtrlSpace
185 self.session.em.sendRIGHT = self.processMove
186 self.session.em.sendLEFT = self.processMove
187 self.session.em.sendUP = self.processMove
188 self.session.em.sendDOWN = self.processMove
189
190
191
192 #---------------------------------------------------------------------------
193 # methods to handle signals for shell I/O
194 # these methods must be async to avoid blocking shell
195 # XXX: signalable object is used.. and it is not really async
196 #---------------------------------------------------------------------------
197
198
199 def replaceWord(self, source, dest, output):
200 #matchesWord = re.findall("(\x1b+\x1b?.*?%s\x1b?)"%source, output)
201 matchesWord = re.findall("(\x1b\[01;34m127.0.0.1\x1b)", output)
202
203 for w in matchesWord:
204 output = output.replace(w, dest)
205
206 return output
207
208 def highligthword(self, w, output):
209 highlighted_word = "\x1b[02;44m%s\x1b[0m" % w
210 output = self.replaceWord(w, highlighted_word, output)
211 return output
212
213 def processOutputHandler(self, output):
214 """
215 This method is called when processOutput signal is emitted
216 It sends the process output to the plugin controller so it can
217 pass it to the corresponding plugin
218 """
219 # the output comes with escape chars for example to show things with colors
220 # those escape chars are messing the text and plugins may not handle that
221 # correctly.
222 #TODO: implement some way of removing the escape sequences
223
224 # if we get the output from the screen image we have some issues when the
225 # output is longer than the actual size and scrolls the window
226 #TODO: check how to handle the window scrolling
227
228 #output = self.session.em.getLastOutputFromScreenImage(1)
229 #api.devlog("-"*50)
230 #api.devlog("processOutputHandler called - output =\n%r" % self.session.em.getLastOutputFromScreenImage(1))
231 #api.devlog("processOutputHandler called - hist lines = %r" % self.session.em._scr.getHistLines())
232 #api.devlog("-"*50)
233
234 #TODO: is this really needed??? (to save first prompt output)
235 if self.__first_process_output:
236 # save the output as prompt
237 #api.devlog("Saving prompt for the first time\n\tPROMPT: %r" % output)
238 # then mark flag because it won't be the first anymore
239 self._initial_prompt = output.strip()
240 self.__first_process_output = False
241 # after getting first output which is the default prompt
242 # we change it and clear screen
243 self.__setCurrentShellPromptFormat()
244 self.session.updateLastUserInputLine()
245 return
246
247 if self.__save_output_prompt_format:
248 # means the output is the PS1 format and we have to store it
249 # The output is the result of running "echo $PS1" so 2 lines are
250 # generated: one with the actual value of PS1 and one more
251 # that is the prompt just because the echo finished
252 # So we have to keep the first line only
253 self._initial_prompt = ouput.splitlines()[0].strip()
254 self.__save_output_prompt_format = False
255
256
257 #strip_control_sequences(output)
258
259
260 #print "AAAAAAAAAAAAAaaa: ", repr(output)
261 #for word in wordsFound:
262 # output = self.highligthword(word, output)
263
264
265 # check if output has to be ignored
266 if not self.__ignore_process_output and not self.__ignore_process_output_once:
267 api.devlog("processOutputHandler (PROCESSED):\n%r" % output)
268
269 command_finished, output = self.check_command_end(output)
270
271 #IMPORTANT: if no plugin was selected to process this output
272 # we don't need to send to controller
273 if self.plugin_controller.getActivePluginStatus():
274 # always send all output to the plugin controller
275 self.plugin_controller.storeCommandOutput(output)
276 # if command ended we notify the plugin
277 if command_finished:
278 api.devlog("calling plugin_controller.onCommandFinished()")
279 self.plugin_controller.onCommandFinished()
280 else:
281 api.devlog("<<< no active plugin...IGNORING OUTPUT >>>")
282
283 else:
284 #if re.search("export PS1",output) == None:
285 # self.__command += output
286
287 #if re.search("export PS1",output) == None:
288 # self.__command += output
289 #
290 #if self.__command != "":
291 # api.devlog("processOutputHandler (Allcommand): (%s)" % self.__command)
292 #
293 # #TODO: hacer un regex inicial, y verificar si es el lugar exacto para poner esto.
294 # #TODO: No soporta el backspace o caracteres especiales
295 # #TODO: Recorrer todo no es performante, hay que revisar
296 # for h in self._model_controller._hosts.itervalues():
297 # if re.search(self.__command,h.name,flags=re.IGNORECASE):
298 # api.devlog("Host name found: " + h.name + " id ("+h.id+")");
299 # for o in h.getAllInterfaces():
300 # if re.search(self.__command,o.name,flags=re.IGNORECASE):
301 # api.devlog("Host name found: " + h.name + " id ("+h.id+") - Interface ("+o.name+") id ("+o.id+")");
302 # for o in h.getAllApplications():
303 # if re.search(self.__command,o.name,flags=re.IGNORECASE):
304 # api.devlog("Host name found: " + h.name + " id ("+h.id+") - Application ("+o.name+") id ("+o.id+")");
305
306
307 api.devlog("processOutputHandler (IGNORED by flags): \n%r" % output)
308 #api.devlog("self.__ignore_process_output_once = %s" % self.__ignore_process_output_once)
309 #api.devlog("self.__ignore_process_output = %s" % self.__ignore_process_output)
310 self.__ignore_process_output_once = False
311
312
313
314
315
316
317 def processMove(self):
318 """
319 this method is called when up/down/left/right
320 """
321
322 if not self.__interactive:
323 self._options=[]
324 self._optionsh={}
325 self._tname = ""
326
327 def processCtrlSpace(self):
328 """
329 this method is called when the Ctrl+Space is pressed
330 """
331 if not self.__interactive:
332 # get the complete user input from screen image (this is done so we don't
333 # have to worry about handling any key)
334 user_input = self.session.em.getLastOutputFromScreenImage(get_spaces=True)
335 # parse input to get the prompt and command in separated parts
336
337 prompt, username, current_path, command_string, command_len = self.__parseUserInput(user_input,get_spaces=True)
338 api.devlog("processCtrlSpace info("+user_input+")("+command_string+")")
339 api.devlog("-"*60)
340 api.devlog("CTRL + SPACE \nprompt = %r\ncommand = %r" % (prompt, command_string))
341 api.devlog("self.__interactive = %s" % self.__interactive )
342 api.devlog("-"*60)
343
344
345 words=command_string.split(" ")
346 #words2=command_string.split(" ")
347 cword=words[len(words)-1] #obtengo la ultima palabra
348 #words.remove(cword) #elimino la ultima palabra
349 options=[]
350 search=0
351 mindex=0
352
353 try: # si encuentra la palabra significa que se encuentra en una interaccion
354 mindex = self._options.index(cword)
355 #api.devlog("El tname es:" + self._tname)
356 # Si no es la misma herramienta o cambio la cantidad de palabra significa que tengo que empezar de nuevo
357 if (self._tname != words[1] and self._tname != "") or (self._lcount != len(words)):
358 mindex = -1
359 except ValueError:
360 mindex = -1
361
362 if mindex == -1: # si no la encuentra inicia de nuevo.
363 self._options=[]
364 self._optionsh={}
365 search=1
366 else:
367 options=self._options
368
369 #Guardo la cantidad palabras para comparar despues
370 self._lcount = len(words)
371
372 #save first command
373 if len(words) >2:
374 self._tname = words[1] #guardo el nombre de la tool
375 else:
376 self._tname = ""
377
378
379 if search ==1 and cword !="":
380 #Busqueda de Hosts (ignore si el comando que escribi es blanco)
381 for h in self._model_controller.getAllHosts():
382 if re.search(str("^"+cword),h.name,flags=re.IGNORECASE):
383 if len(options) == 0:
384 options.append(cword)
385 api.devlog("Host name found: " + h.name + " id ("+h.id+")");
386 options.append(h.name)
387 #Busqueda de Hostname dentro de las interfaces
388 for i in h.getAllInterfaces():
389 for hostname in i.getHostnames():
390 if re.search(str("^"+cword),hostname,flags=re.IGNORECASE):
391 if len(options) == 0:
392 options.append(cword)
393 api.devlog("Hostname found: " + hostname + " id ("+i.id+")");
394 options.append(hostname)
395
396 self._options = options
397
398 new_options={}
399 api.devlog("Cantidad de _options" + str(len(self._options)))
400
401 #Si no se encontro nada, busco opciones en el plugin
402 if len(self._options) == 0:
403 #try:
404 if 1==1:
405 #Llamo al controller para ver si hay algun plugin que pueda dar opciones
406 #Devuelve un dict del estilo 'option' : 'help de la option'
407 new_options = self.plugin_controller.getPluginAutocompleteOptions(prompt, username,
408 current_path,
409 command_string,
410 self.__interactive)
411
412
413 if new_options != None:
414 if len(new_options) >= 1: #Si encontro plugin que maneje y trae opciones hago iteracciones.
415 api.devlog("Options de plugin encontradas: ("+str(len(new_options))+") valores ("+str(new_options)+")")
416 options = [cword]+new_options.keys() #Guardo las opciones (agrego la word inicial)
417 self._options = options
418 self._optionsh = new_options
419
420 api.devlog("getPluginAutocompleteOptions: %r" % user_input)
421 api.devlog("new_options:" + str(options))
422 if 1==2:
423 #except Exception:
424 api.devlog("Exception: Plugin")
425 # if anything in the plugins fails and raises an exception we continue wihout doing anything
426 new_cmd = None
427
428 # Recorro las opciones disponibles
429 #TODO: Reemplazar esto por una ventana desplegable o
430 i=0
431 newword=""
432 if len(options) > 1: # Reemplazar solo si hay opciones
433 for w in options:
434 #api.devlog("Por la palabra ("+ w +") (" + str(i)+") la palabra(" + cword+")")
435 if cword==w:
436 if len(options) > i+1:
437 newword=options[i+1]
438 #api.devlog("La encontre next ("+ newword +") (" + str(i)+")"+ str(options) )
439 else:
440 newword=options[0]
441 #api.devlog("La encontre last ("+ newword +") (" + str(i)+")"+ str(options) )
442 #newword="-h"
443 i+=1
444
445 if self._optionsh.has_key(newword):
446 #TODO: reemplazar esto por un help distinto no usar el devlog
447 api.showPopup( newword + " :" + self._optionsh[newword])
448 #api.devlog("pluginhelp: " + newword + " :" + self._optionsh[newword])
449
450 #Hago el cambio en la shell
451 self.session.sh.sendBytes("\b" * len(cword) + newword)
452
453 def processUserInputBuffer(self):
454 """
455 this method is called when the ENTER is pressed
456 It processes the user input buffer and then it clears it for future
457 if a new command is returned by a plugin this is returned to the caller
458 (which is onKeyPress in module emuVt102)
459 """
460
461 command_string=""
462 command_len = 0
463 if not self.__interactive:
464 # get the complete user input from screen image (this is done so we don't
465 # have to worry about handling any key)
466 user_input = self.session.em.getLastOutputFromScreenImage(get_full_content=True)
467 api.devlog("user_input parsed from screen(0) = %s" % user_input)
468 # parse input to get the prompt and command in separated parts
469 prompt, username, current_path, command_string, command_len = self.__parseUserInput(user_input)
470
471 api.devlog("user_input parsed from screen(1) =%s" % self.session.em.getLastOutputFromScreenImage(index=1, get_full_content=True))
472
473 # we send the buffer to the plugin controller to determine
474 # if there is a plugin suitable to handle it
475 api.devlog("-"*60)
476 api.devlog("about to call plugin controller\nprompt = %r\ncommand = %r" % (prompt, command_string))
477 api.devlog("self.__interactive = %s" % self.__interactive )
478 api.devlog("-"*60)
479 # when calling the plugin, the command string may be changed
480 # if the configuration allows this we send it instead of the typed one
481 #TODO: validate config to allow this
482
483 try:
484 new_cmd = self.plugin_controller.processCommandInput(prompt, username,
485 current_path,
486 command_string,
487 self.__interactive)
488
489 # we set it to interactive until we make sure the command has finished
490 # this check is done in processOutputHandler
491 self.__interactive = True
492 api.devlog("processUserInputBuffer: %r" % user_input)
493 api.devlog("new_cmd: %r" % new_cmd)
494 except Exception, e:
495 # if anything in the plugins fails and raises an exception we continue wihout doing anything
496 api.devlog("ERROR: processCommandString")
497 api.devlog(e)
498 new_cmd = None
499
500 if new_cmd is None:
501 # the output MUST BE processed
502 self.__ignore_process_output = False
503 self.__ignore_process_output_once = False
504 else:
505 # means the plugin changed command and we are going to send
506 # ALT + r to delete current line. That produces an empty output
507 # which has to be ignored
508
509 self.__ignore_process_output_once = True
510 self.__ignore_process_output = False
511 else:
512 api.devlog("IGNORING BECAUSE INTERACTIVE FLAG WAS SET")
513 new_cmd = None
514
515
516 return new_cmd,command_string,command_len
517
518 def ignoreDueResize(self):
519 self.__ignore_process_output = True
520
521
522 def __parseUserInput(self, user_input="", get_spaces=False):
523 """
524 parses the user input buffer and returns the following values:
525 * current user prompt
526 * current username
527 * current path
528 * command or text after prompt
529 """
530 username = ""
531 hostname = ""
532 current_path = ""
533 usr_prompt = ""
534 usr_command = ""
535 raw_command = ""
536
537 match = self._custom_prompt_format.search(user_input)
538 if match is not None:
539 username = match.group("user")
540 hostname = match.group("host")
541 current_path = os.path.expanduser(match.group("path"))
542 usr_prompt = user_input[:match.end()].strip()
543 raw_command = user_input[match.end():]
544 if get_spaces == False:
545 usr_command = raw_command.strip()
546 else:
547 usr_command = raw_command
548 else:
549 # means there was no prompt and theres only user input
550 usr_command = user_input
551
552 return usr_prompt, username, current_path, usr_command, len(raw_command)
553
554 def __setCurrentShellPromptFormat(self):
555 # this is done to be able to detect the prompt later
556 # Since we defined the prompt format we know how to parse it
557 # The prompt format must not contain color escape chars or it'll mess up the output
558 self.session.set_shell_ps1("[\\u@\\h:\\w]>\\$ ")
559
560 def __getCurrentShellPromptFormat(self):
561 """
562 this gets the current PS1 environment variable
563 of the current shell created.
564 This is async because a command is sent and
565 the output is retrieved later.
566 """
567 self.__save_output_prompt_format = True
568 self.session.get_shell_ps1()
569
570
571 def __matchesCustomPrompt(self, txt):
572 """
573 checks if the current text matches our custom prompt format
574 and returns true in that case, false otherwise
575 """
576 if not self._custom_prompt_format:
577 api.devlog("prompt format (PS1) is not defined.\nThis may cause unexpected results...")
578 return False
579
580 txt = txt.strip()
581 m = self._custom_prompt_format.search(txt)
582 return (m is not None)
583
584 #XXX: this code below checked that the match was the last part of the text
585 #if m is not None:
586 # if len(txt) == m.end():
587 # return True
588 #return False
589
590 def __matchGenericPrompt(self, txt):
591 """
592 checks if the current text matches against a list of
593 generic prompt formats defined in the configuration
594 and returns true in that case, false otherwise.
595 This is used because if a prompt is detected it may be because a connection
596 was established with a remote host.
597 """
598 if not self._generic_prompt_formats:
599 api.devlog("There isn't any generic prompt format defined")
600 return False
601
602 txt = txt.strip()
603 # Should we use match instead of search?
604 for r in self._generic_prompt_formats:
605 m = r.search(txt)
606 if m is not None:
607 return True
608 return False
609
610
611 def check_command_end(self, output):
612 """
613 Checks if the command finished by checking if the last line of the ouput
614 is just our shell custom prompt.
615 It also checks if a generic prompt is detected as the last line, which
616 could mean that the commmand may have resulted in a remote connection
617 to another host or device.
618 This method returns 2 values: first a boolean flag that determines if
619 command ended and then the full command output.
620 """
621 # We check if the last line in the output is just our modified shell prompt...
622 # This would mean the command ended and we notify the plugin then
623 api.devlog("check_command_end called...\noutput received = %r" % output)
624 output_lines = output.splitlines()
625 last_line = output_lines[-1].strip()
626 api.devlog("about to test match with line %r" % last_line)
627 command_finished = self.__matchesCustomPrompt(last_line)
628 #command_finished = self.__matchesCustomPrompt(last_line.split()[0])
629 if command_finished:
630 # if we found this prompt then it means the command ended
631 # we remove that line from output to send it
632 api.devlog("command finished. Removing last line from output because it is just the prompt")
633 output_lines.pop(-1)
634 output = "\n".join(output_lines)
635 # if command finished we need to ignore further output. It will be user input
636 self.__ignore_process_output = True
637 self.__interactive = False
638 self.session.updateLastUserInputLine()
639 else:
640 # if we are here means that last line of the output is not our custom
641 # shell prompt, but we need to check if a generic prompt is there
642 # which means a remote connection may have been established
643 if self.__matchGenericPrompt(last_line):
644 api.devlog("A generic prompt format matched the last line of the command ouput")
645 #TODO: define what to do in this case
646
647 return command_finished, output
648
649 def terminate(self):
650 if self.__running:
651 self.__running = False
652 self.session.terminate()
653 #-------------------------------------------------------------------------------
0 #!/usr/bin/env python
1 '''
2 Faraday Penetration Test IDE
3 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
4 See the file 'doc/LICENSE' for the license information
5
6 '''
7
8 import os
9 import pwd
10 import re
11 from cStringIO import StringIO
12
13 from model.common import ModelObject
14 from shell.core.history import HistoryTypeBuffer
15 import model.api as api
16
17 from ecma48 import strip_control_sequences
18 #TODO: check from config if it is qt3 or qt4 and import the right one
19 from gui.qt3.pyqonsole.widget import ShellWidget
20 from shell.controller.qt3.session import Session
21 #from shell.controller.qt3.procctrl import ProcessController
22 from model.common import TreeWordsTries
23 from config.configuration import getInstanceConfiguration
24 CONF = getInstanceConfiguration()
25
26 next_id = 1
27 #-------------------------------------------------------------------------------
28
29 #TODO: decide if we really need to inherit from ModelObject
30 class ShellEnvironment(ModelObject):
31 """
32 Shell environment is really a group of components that need to work together
33 The environment is composed by:
34 * a ShellWidget used in the GUI
35 * a Session (which is the real shell)
36 * a PluginController needed to handle all shell input and output
37 The shell environment interacts with the Model Controller to add new hosts
38 """
39 def __init__(self, name, qapp, gui_parent, model_controller,
40 plugin_controller, close_callback=None):
41
42 ModelObject.__init__(self)
43 self._id = self.get_id()
44 self.name = name
45
46 # a reference used to add new hosts
47 self._model_controller = model_controller
48
49 # create the widget
50 self.widget = ShellWidget(qapp,gui_parent,name)
51 self.widget.setupLayout()
52
53 # For safesty we don't use the user shell, we force it to bash
54 progname = "/bin/bash"
55 # create the session
56 #Session(gui, pgm, args, term, sessionid='session-1', cwd=None):
57 #self.process_controller = ProcessController()
58 self.session = Session(self.widget, progname, [], "xterm", name);
59
60 self.session.setConnect(True)
61 self.session.setHistory(HistoryTypeBuffer(1000))
62 self._setUpSessionSinalHandlers()
63
64 #self.__last_user_input = None
65 #self.__user_input_signal = False
66
67 # flag that determines if output has to be ignored
68 self.__ignore_process_output = False
69 self.__ignore_process_output_once = False
70 self.__first_process_output = True
71 self.__save_output_prompt_format = False
72
73 # determines if input is for an interactive command or not
74 self.__interactive = False
75
76 #TODO: check if we need to connect to this signal
77 self.session.myconnect('done', self.close)
78
79 # instance a new plugin controller
80 self.plugin_controller = plugin_controller(self.id)
81
82 self._initial_prompt = ""
83 #self._custom_prompt_format = re.compile("\\x1B\[1;32m\[.+@.+\s.+\]>(\$|#)\s+\\x1B\[m")
84 #self._custom_prompt_format = re.compile("\[.+@.+\s.+\]>(\$|#)\s+")
85 self._custom_prompt_format = re.compile("\[(?P<user>.+)@(?P<host>.+):(?P<path>.+)\]>(\$|#)")
86 #TODO: somewhere in the config there should be a list of regexes used to match
87 # prompts. Be careful with this because if regexes are too generic they will
88 # match more that is needed.
89 self._generic_prompt_formats = []
90 #XXX: adding this format just as a test! This should be in the config somehow
91 # Also this may be is too generic to use...
92 self._generic_prompt_formats.append(re.compile("^.+@.+:.+(\$|#)$"))
93
94 #This is a reference to MainApplication.deleteShellEnvironment
95 self._close_callback = close_callback
96
97 # flag that determines if shell environment is running
98 self.__running = False
99
100 #Autocomplete
101 self._options=[] #only keys
102 self._tname="" # last tool executed
103 self._lcount=0
104 self._optionsh={} #keys + help
105 self.__command = ''
106 def __del__(self):
107 """
108 When deleteing the environment we have to delete all the components
109 """
110 #print "ShellEnvironment __del__ called"
111 del self.session
112 #TODO: delete tab holding current ShellWidget
113 #tabmgr = self.widget.parentWidget()
114 #tabmgr.removeView(self.widget)
115 #FIXME: check the __del__ method in ShellWidget
116 #del self.widget
117 #TODO: make sure the plugin controller correctly finishes all processes
118 del self.plugin_controller
119
120 def close(self, session, status, *args):
121 #TODO: por alguna razon queda colgado un QSocketNotifier
122 # QSocketNotifier: invalid socket 17 and type 'Read', disabling...
123 # y eso cuelga la aplicacion
124 api.devlog("ShellEnvironment close was called - session = %r, status = %r , *args = %r" % (session, status, args))
125 if self._close_callback is not None:
126 self._close_callback(self.name, self)
127 else:
128 api.devlog("close was call but callback is not set")
129
130 #def setCloseCallback(self, ref):
131 # self._close_callback = ref
132
133 def get_id(self):
134 global next_id
135 # just to make sure each env has a differente id because names can be the same
136 id = next_id
137 next_id += 1
138 return id
139
140 def run(self):
141 self.session.run()
142 self.__running = True
143 #self.plugin_controller.run()
144
145 def _setUpSessionSinalHandlers(self):
146 """
147 Connects some signal handlers to different signals emmited inside shell
148 emulation and pty.
149 These signals are used to handle user input and child process output
150 """
151 #IMPORTANT ABOUT USER INPUT AND PROCESS OUPUT (Session class)
152 #PtyProcess in method dataReceived gets the child output
153 #in method sendBytes it has the user input
154 #
155 #Emulation in method sendString also has the user input
156 #also check method onKeyPressed to identify when an ENTER key is pressed
157 #
158 #
159 #OUTPUT
160 # emulation onRcvBlock le llega la salida del proceso
161 # self.myconnect('receivedStdout', self.dataReceived)
162 # Este se llama desde childOutput en pty_
163 # self.myemit("receivedStdout", (fdno, lenlist))
164 #
165 # self.myemit('block_in', (buf,)) <--- se llama desde dataReceived
166 # self.sh.myconnect('block_in', self.em.onRcvBlock)
167 #
168 #INPUT
169 # self.myemit("sndBlock", (string,))
170 # self.em.myconnect('sndBlock', self.sh.sendBytes)
171 #
172 # otra opcion es agregar una senal propia en los metodos sendString
173 # o en onKeyPressed de emuVt102
174
175 # connect signals to handle shell I/O
176 self.session.sh.myconnect('processOutput', self.processOutputHandler)
177
178 #XXX: nasty hack to be able to send a return value
179 # Using myconnect and emitting singals won't allow us to return a value
180 self.session.em.sendENTER = self.processUserInputBuffer
181 self.widget.myconnect('ignoreShellWidgetResize', self.ignoreDueResize)
182
183 #handle ctrl+space
184 self.session.em.sendCTRLSPACE = self.processCtrlSpace
185 self.session.em.sendRIGHT = self.processMove
186 self.session.em.sendLEFT = self.processMove
187 self.session.em.sendUP = self.processMove
188 self.session.em.sendDOWN = self.processMove
189
190
191
192 #---------------------------------------------------------------------------
193 # methods to handle signals for shell I/O
194 # these methods must be async to avoid blocking shell
195 # XXX: signalable object is used.. and it is not really async
196 #---------------------------------------------------------------------------
197
198
199 def replaceWord(self, source, dest, output):
200 #matchesWord = re.findall("(\x1b+\x1b?.*?%s\x1b?)"%source, output)
201 matchesWord = re.findall("(\x1b\[01;34m127.0.0.1\x1b)", output)
202
203 for w in matchesWord:
204 output = output.replace(w, dest)
205
206 return output
207
208 def highligthword(self, w, output):
209 highlighted_word = "\x1b[02;44m%s\x1b[0m" % w
210 output = self.replaceWord(w, highlighted_word, output)
211 return output
212
213 def processOutputHandler(self, output):
214 """
215 This method is called when processOutput signal is emitted
216 It sends the process output to the plugin controller so it can
217 pass it to the corresponding plugin
218 """
219 # the output comes with escape chars for example to show things with colors
220 # those escape chars are messing the text and plugins may not handle that
221 # correctly.
222 #TODO: implement some way of removing the escape sequences
223
224 # if we get the output from the screen image we have some issues when the
225 # output is longer than the actual size and scrolls the window
226 #TODO: check how to handle the window scrolling
227
228 #output = self.session.em.getLastOutputFromScreenImage(1)
229 #api.devlog("-"*50)
230 #api.devlog("processOutputHandler called - output =\n%r" % self.session.em.getLastOutputFromScreenImage(1))
231 #api.devlog("processOutputHandler called - hist lines = %r" % self.session.em._scr.getHistLines())
232 #api.devlog("-"*50)
233
234 #TODO: is this really needed??? (to save first prompt output)
235 if self.__first_process_output:
236 # save the output as prompt
237 #api.devlog("Saving prompt for the first time\n\tPROMPT: %r" % output)
238 # then mark flag because it won't be the first anymore
239 self._initial_prompt = output.strip()
240 self.__first_process_output = False
241 # after getting first output which is the default prompt
242 # we change it and clear screen
243 self.__setCurrentShellPromptFormat()
244 self.session.updateLastUserInputLine()
245 return
246
247 if self.__save_output_prompt_format:
248 # means the output is the PS1 format and we have to store it
249 # The output is the result of running "echo $PS1" so 2 lines are
250 # generated: one with the actual value of PS1 and one more
251 # that is the prompt just because the echo finished
252 # So we have to keep the first line only
253 self._initial_prompt = ouput.splitlines()[0].strip()
254 self.__save_output_prompt_format = False
255
256
257 #strip_control_sequences(output)
258
259
260 #print "AAAAAAAAAAAAAaaa: ", repr(output)
261 #for word in wordsFound:
262 # output = self.highligthword(word, output)
263
264
265 # check if output has to be ignored
266 if not self.__ignore_process_output and not self.__ignore_process_output_once:
267 api.devlog("processOutputHandler (PROCESSED):\n%r" % output)
268
269 command_finished, output = self.check_command_end(output)
270
271 #IMPORTANT: if no plugin was selected to process this output
272 # we don't need to send to controller
273 if self.plugin_controller.getActivePluginStatus():
274 # always send all output to the plugin controller
275 self.plugin_controller.storeCommandOutput(output)
276 # if command ended we notify the plugin
277 if command_finished:
278 api.devlog("calling plugin_controller.onCommandFinished()")
279 self.plugin_controller.onCommandFinished()
280 else:
281 api.devlog("<<< no active plugin...IGNORING OUTPUT >>>")
282
283 else:
284 #if re.search("export PS1",output) == None:
285 # self.__command += output
286
287 #if re.search("export PS1",output) == None:
288 # self.__command += output
289 #
290 #if self.__command != "":
291 # api.devlog("processOutputHandler (Allcommand): (%s)" % self.__command)
292 #
293 # #TODO: hacer un regex inicial, y verificar si es el lugar exacto para poner esto.
294 # #TODO: No soporta el backspace o caracteres especiales
295 # #TODO: Recorrer todo no es performante, hay que revisar
296 # for h in self._model_controller._hosts.itervalues():
297 # if re.search(self.__command,h.name,flags=re.IGNORECASE):
298 # api.devlog("Host name found: " + h.name + " id ("+h.id+")");
299 # for o in h.getAllInterfaces():
300 # if re.search(self.__command,o.name,flags=re.IGNORECASE):
301 # api.devlog("Host name found: " + h.name + " id ("+h.id+") - Interface ("+o.name+") id ("+o.id+")");
302 # for o in h.getAllApplications():
303 # if re.search(self.__command,o.name,flags=re.IGNORECASE):
304 # api.devlog("Host name found: " + h.name + " id ("+h.id+") - Application ("+o.name+") id ("+o.id+")");
305
306
307 api.devlog("processOutputHandler (IGNORED by flags): \n%r" % output)
308 #api.devlog("self.__ignore_process_output_once = %s" % self.__ignore_process_output_once)
309 #api.devlog("self.__ignore_process_output = %s" % self.__ignore_process_output)
310 self.__ignore_process_output_once = False
311
312
313
314
315
316
317 def processMove(self):
318 """
319 this method is called when up/down/left/right
320 """
321
322 if not self.__interactive:
323 self._options=[]
324 self._optionsh={}
325 self._tname = ""
326
327 def processCtrlSpace(self):
328 """
329 this method is called when the Ctrl+Space is pressed
330 """
331 if not self.__interactive:
332 # get the complete user input from screen image (this is done so we don't
333 # have to worry about handling any key)
334 user_input = self.session.em.getLastOutputFromScreenImage(get_spaces=True)
335 # parse input to get the prompt and command in separated parts
336
337 prompt, username, current_path, command_string, command_len = self.__parseUserInput(user_input,get_spaces=True)
338 api.devlog("processCtrlSpace info("+user_input+")("+command_string+")")
339 api.devlog("-"*60)
340 api.devlog("CTRL + SPACE \nprompt = %r\ncommand = %r" % (prompt, command_string))
341 api.devlog("self.__interactive = %s" % self.__interactive )
342 api.devlog("-"*60)
343
344
345 words=command_string.split(" ")
346 #words2=command_string.split(" ")
347 cword=words[len(words)-1] #obtengo la ultima palabra
348 #words.remove(cword) #elimino la ultima palabra
349 options=[]
350 search=0
351 mindex=0
352
353 try: # si encuentra la palabra significa que se encuentra en una interaccion
354 mindex = self._options.index(cword)
355 #api.devlog("El tname es:" + self._tname)
356 # Si no es la misma herramienta o cambio la cantidad de palabra significa que tengo que empezar de nuevo
357 if (self._tname != words[1] and self._tname != "") or (self._lcount != len(words)):
358 mindex = -1
359 except ValueError:
360 mindex = -1
361
362 if mindex == -1: # si no la encuentra inicia de nuevo.
363 self._options=[]
364 self._optionsh={}
365 search=1
366 else:
367 options=self._options
368
369 #Guardo la cantidad palabras para comparar despues
370 self._lcount = len(words)
371
372 #save first command
373 if len(words) >2:
374 self._tname = words[1] #guardo el nombre de la tool
375 else:
376 self._tname = ""
377
378
379 if search ==1 and cword !="":
380 #Busqueda de Hosts (ignore si el comando que escribi es blanco)
381 for h in self._model_controller.getAllHosts():
382 if re.search(str("^"+cword),h.name,flags=re.IGNORECASE):
383 if len(options) == 0:
384 options.append(cword)
385 api.devlog("Host name found: " + h.name + " id ("+h.id+")");
386 options.append(h.name)
387 #Busqueda de Hostname dentro de las interfaces
388 for i in h.getAllInterfaces():
389 for hostname in i.getHostnames():
390 if re.search(str("^"+cword),hostname,flags=re.IGNORECASE):
391 if len(options) == 0:
392 options.append(cword)
393 api.devlog("Hostname found: " + hostname + " id ("+i.id+")");
394 options.append(hostname)
395
396 self._options = options
397
398 new_options={}
399 api.devlog("Cantidad de _options" + str(len(self._options)))
400
401 #Si no se encontro nada, busco opciones en el plugin
402 if len(self._options) == 0:
403 #try:
404 if 1==1:
405 #Llamo al controller para ver si hay algun plugin que pueda dar opciones
406 #Devuelve un dict del estilo 'option' : 'help de la option'
407 new_options = self.plugin_controller.getPluginAutocompleteOptions(prompt, username,
408 current_path,
409 command_string,
410 self.__interactive)
411
412
413 if new_options != None:
414 if len(new_options) >= 1: #Si encontro plugin que maneje y trae opciones hago iteracciones.
415 api.devlog("Options de plugin encontradas: ("+str(len(new_options))+") valores ("+str(new_options)+")")
416 options = [cword]+new_options.keys() #Guardo las opciones (agrego la word inicial)
417 self._options = options
418 self._optionsh = new_options
419
420 api.devlog("getPluginAutocompleteOptions: %r" % user_input)
421 api.devlog("new_options:" + str(options))
422 if 1==2:
423 #except Exception:
424 api.devlog("Exception: Plugin")
425 # if anything in the plugins fails and raises an exception we continue wihout doing anything
426 new_cmd = None
427
428 # Recorro las opciones disponibles
429 #TODO: Reemplazar esto por una ventana desplegable o
430 i=0
431 newword=""
432 if len(options) > 1: # Reemplazar solo si hay opciones
433 for w in options:
434 #api.devlog("Por la palabra ("+ w +") (" + str(i)+") la palabra(" + cword+")")
435 if cword==w:
436 if len(options) > i+1:
437 newword=options[i+1]
438 #api.devlog("La encontre next ("+ newword +") (" + str(i)+")"+ str(options) )
439 else:
440 newword=options[0]
441 #api.devlog("La encontre last ("+ newword +") (" + str(i)+")"+ str(options) )
442 #newword="-h"
443 i+=1
444
445 if self._optionsh.has_key(newword):
446 #TODO: reemplazar esto por un help distinto no usar el devlog
447 api.showPopup( newword + " :" + self._optionsh[newword])
448 #api.devlog("pluginhelp: " + newword + " :" + self._optionsh[newword])
449
450 #Hago el cambio en la shell
451 self.session.sh.sendBytes("\b" * len(cword) + newword)
452
453 def processUserInputBuffer(self):
454 """
455 this method is called when the ENTER is pressed
456 It processes the user input buffer and then it clears it for future
457 if a new command is returned by a plugin this is returned to the caller
458 (which is onKeyPress in module emuVt102)
459 """
460
461 command_string=""
462 command_len = 0
463 if not self.__interactive:
464 # get the complete user input from screen image (this is done so we don't
465 # have to worry about handling any key)
466 user_input = self.session.em.getLastOutputFromScreenImage(get_full_content=True)
467 api.devlog("user_input parsed from screen(0) = %s" % user_input)
468 # parse input to get the prompt and command in separated parts
469 prompt, username, current_path, command_string, command_len = self.__parseUserInput(user_input)
470
471 api.devlog("user_input parsed from screen(1) =%s" % self.session.em.getLastOutputFromScreenImage(index=1, get_full_content=True))
472
473 # we send the buffer to the plugin controller to determine
474 # if there is a plugin suitable to handle it
475 api.devlog("-"*60)
476 api.devlog("about to call plugin controller\nprompt = %r\ncommand = %r" % (prompt, command_string))
477 api.devlog("self.__interactive = %s" % self.__interactive )
478 api.devlog("-"*60)
479 # when calling the plugin, the command string may be changed
480 # if the configuration allows this we send it instead of the typed one
481 #TODO: validate config to allow this
482
483 try:
484 new_cmd = self.plugin_controller.processCommandInput(prompt, username,
485 current_path,
486 command_string,
487 self.__interactive)
488
489 # we set it to interactive until we make sure the command has finished
490 # this check is done in processOutputHandler
491 self.__interactive = True
492 api.devlog("processUserInputBuffer: %r" % user_input)
493 api.devlog("new_cmd: %r" % new_cmd)
494 except Exception, e:
495 # if anything in the plugins fails and raises an exception we continue wihout doing anything
496 api.devlog("ERROR: processCommandString")
497 api.devlog(e)
498 new_cmd = None
499
500 if new_cmd is None:
501 # the output MUST BE processed
502 self.__ignore_process_output = False
503 self.__ignore_process_output_once = False
504 else:
505 # means the plugin changed command and we are going to send
506 # ALT + r to delete current line. That produces an empty output
507 # which has to be ignored
508
509 self.__ignore_process_output_once = True
510 self.__ignore_process_output = False
511 else:
512 api.devlog("IGNORING BECAUSE INTERACTIVE FLAG WAS SET")
513 new_cmd = None
514
515
516 return new_cmd,command_string,command_len
517
518 def ignoreDueResize(self):
519 self.__ignore_process_output = True
520
521
522 def __parseUserInput(self, user_input="", get_spaces=False):
523 """
524 parses the user input buffer and returns the following values:
525 * current user prompt
526 * current username
527 * current path
528 * command or text after prompt
529 """
530 username = ""
531 hostname = ""
532 current_path = ""
533 usr_prompt = ""
534 usr_command = ""
535 raw_command = ""
536
537 match = self._custom_prompt_format.search(user_input)
538 if match is not None:
539 username = match.group("user")
540 hostname = match.group("host")
541 current_path = os.path.expanduser(match.group("path"))
542 usr_prompt = user_input[:match.end()].strip()
543 raw_command = user_input[match.end():]
544 if get_spaces == False:
545 usr_command = raw_command.strip()
546 else:
547 usr_command = raw_command
548 else:
549 # means there was no prompt and theres only user input
550 usr_command = user_input
551
552 return usr_prompt, username, current_path, usr_command, len(raw_command)
553
554 def __setCurrentShellPromptFormat(self):
555 # this is done to be able to detect the prompt later
556 # Since we defined the prompt format we know how to parse it
557 # The prompt format must not contain color escape chars or it'll mess up the output
558 self.session.set_shell_ps1("[\\u@\\h:\\w]>\\$ ")
559
560 def __getCurrentShellPromptFormat(self):
561 """
562 this gets the current PS1 environment variable
563 of the current shell created.
564 This is async because a command is sent and
565 the output is retrieved later.
566 """
567 self.__save_output_prompt_format = True
568 self.session.get_shell_ps1()
569
570
571 def __matchesCustomPrompt(self, txt):
572 """
573 checks if the current text matches our custom prompt format
574 and returns true in that case, false otherwise
575 """
576 if not self._custom_prompt_format:
577 api.devlog("prompt format (PS1) is not defined.\nThis may cause unexpected results...")
578 return False
579
580 txt = txt.strip()
581 m = self._custom_prompt_format.search(txt)
582 return (m is not None)
583
584 #XXX: this code below checked that the match was the last part of the text
585 #if m is not None:
586 # if len(txt) == m.end():
587 # return True
588 #return False
589
590 def __matchGenericPrompt(self, txt):
591 """
592 checks if the current text matches against a list of
593 generic prompt formats defined in the configuration
594 and returns true in that case, false otherwise.
595 This is used because if a prompt is detected it may be because a connection
596 was established with a remote host.
597 """
598 if not self._generic_prompt_formats:
599 api.devlog("There isn't any generic prompt format defined")
600 return False
601
602 txt = txt.strip()
603 # Should we use match instead of search?
604 for r in self._generic_prompt_formats:
605 m = r.search(txt)
606 if m is not None:
607 return True
608 return False
609
610
611 def check_command_end(self, output):
612 """
613 Checks if the command finished by checking if the last line of the ouput
614 is just our shell custom prompt.
615 It also checks if a generic prompt is detected as the last line, which
616 could mean that the commmand may have resulted in a remote connection
617 to another host or device.
618 This method returns 2 values: first a boolean flag that determines if
619 command ended and then the full command output.
620 """
621 # We check if the last line in the output is just our modified shell prompt...
622 # This would mean the command ended and we notify the plugin then
623 api.devlog("check_command_end called...\noutput received = %r" % output)
624 output_lines = output.splitlines()
625 last_line = output_lines[-1].strip()
626 api.devlog("about to test match with line %r" % last_line)
627 command_finished = self.__matchesCustomPrompt(last_line)
628 #command_finished = self.__matchesCustomPrompt(last_line.split()[0])
629 if command_finished:
630 # if we found this prompt then it means the command ended
631 # we remove that line from output to send it
632 api.devlog("command finished. Removing last line from output because it is just the prompt")
633 output_lines.pop(-1)
634 output = "\n".join(output_lines)
635 # if command finished we need to ignore further output. It will be user input
636 self.__ignore_process_output = True
637 self.__interactive = False
638 self.session.updateLastUserInputLine()
639 else:
640 # if we are here means that last line of the output is not our custom
641 # shell prompt, but we need to check if a generic prompt is there
642 # which means a remote connection may have been established
643 if self.__matchGenericPrompt(last_line):
644 api.devlog("A generic prompt format matched the last line of the command ouput")
645 #TODO: define what to do in this case
646
647 return command_finished, output
648
649 def terminate(self):
650 if self.__running:
651 self.__running = False
652 self.session.terminate()
653 #-------------------------------------------------------------------------------
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 """
7 * This is an implementation of wcwidth() and wcswidth() (defined in
8 * IEEE Std 1002.1-2001) for Unicode.
9 *
10 * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html
11 * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html
12 *
13 * In fixed-width output devices, Latin characters all occupy a single
14 * "cell" position of equal width, whereas ideographic CJK characters
15 * occupy two such cells. Interoperability between terminal-line
16 * applications and (teletype-style) character terminals using the
17 * UTF-8 encoding requires agreement on which character should advance
18 * the cursor by how many cell positions. No established formal
19 * standards exist at present on which Unicode character shall occupy
20 * how many cell positions on character terminals. These routines are
21 * a first attempt of defining such behavior based on simple rules
22 * applied to data provided by the Unicode Consortium.
23 *
24 * For some graphical characters, the Unicode standard explicitly
25 * defines a character-cell width via the definition of the East Asian
26 * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes.
27 * In all these cases, there is no ambiguity about which width a
28 * terminal shall use. For characters in the East Asian Ambiguous (A)
29 * class, the width choice depends purely on a preference of backward
30 * compatibility with either historic CJK or Western practice.
31 * Choosing single-width for these characters is easy to justify as
32 * the appropriate long-term solution, as the CJK practice of
33 * displaying these characters as double-width comes from historic
34 * implementation simplicity (8-bit encoded characters were displayed
35 * single-width and 16-bit ones double-width, even for Greek,
36 * Cyrillic, etc.) and not any typographic considerations.
37 *
38 * Much less clear is the choice of width for the Not East Asian
39 * (Neutral) class. Existing practice does not dictate a width for any
40 * of these characters. It would nevertheless make sense
41 * typographically to allocate two character cells to characters such
42 * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be
43 * represented adequately with a single-width glyph. The following
44 * routines at present merely assign a single-cell width to all
45 * neutral characters, in the interest of simplicity. This is not
46 * entirely satisfactory and should be reconsidered before
47 * establishing a formal standard in this area. At the moment, the
48 * decision which Not East Asian (Neutral) characters should be
49 * represented by double-width glyphs cannot yet be answered by
50 * applying a simple rule from the Unicode database content. Setting
51 * up a proper standard for the behavior of UTF-8 character terminals
52 * will require a careful analysis not only of each Unicode character,
53 * but also of each presentation form, something the author of these
54 * routines has avoided to do so far.
55 *
56 * http://www.unicode.org/unicode/reports/tr11/
57 *
58 * Markus Kuhn -- 2003-05-20 (Unicode 4.0)
59 *
60 * Permission to use, copy, modify, and distribute this software
61 * for any purpose and without fee is hereby granted. The author
62 * disclaims all warranties with regard to this software.
63 *
64 * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
65
66 """
67
68
69 # auxiliary function for binary search in interval table
70 def _bisearch(ucs, table):
71 min = 0
72 max = len(table)-1
73
74 if ucs < table[0][0] or ucs > table[max][1]:
75 return 0
76 while max >= min:
77 mid = (min + max) / 2
78 if ucs > table[mid][1]:
79 min = mid + 1
80 elif ucs < table[mid][0]:
81 max = mid - 1
82 else:
83 return 1
84
85 return 0
86
87 """
88 * The following two functions define the column width of an ISO 10646
89 * character as follows:
90 *
91 * - The null character (U+0000) has a column width of 0.
92 *
93 * - Other C0/C1 control characters and DEL will lead to a return
94 * value of -1.
95 *
96 * - Non-spacing and enclosing combining characters (general
97 * category code Mn or Me in the Unicode database) have a
98 * column width of 0.
99 *
100 * - SOFT HYPHEN (U+00AD) has a column width of 1.
101 *
102 * - Other format characters (general category code Cf in the Unicode
103 * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
104 *
105 * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
106 * have a column width of 0.
107 *
108 * - Spacing characters in the East Asian Wide (W) or East Asian
109 * Full-width (F) category as defined in Unicode Technical
110 * Report #11 have a column width of 2.
111 *
112 * - All remaining characters (including all printable
113 * ISO 8859-1 and WGL4 characters, Unicode control characters,
114 * etc.) have a column width of 1.
115 *
116 * This implementation assumes that wchar_t characters are encoded
117 * in ISO 10646.
118 """
119
120 """
121 sorted list of non-overlapping intervals of non-spacing characters
122 generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c"
123 """
124
125 _combining = [
126 ( 0x0300, 0x0357 ), ( 0x035D, 0x036F ), ( 0x0483, 0x0486 ),
127 ( 0x0488, 0x0489 ), ( 0x0591, 0x05A1 ), ( 0x05A3, 0x05B9 ),
128 ( 0x05BB, 0x05BD ), ( 0x05BF, 0x05BF ), ( 0x05C1, 0x05C2 ),
129 ( 0x05C4, 0x05C4 ), ( 0x0600, 0x0603 ), ( 0x0610, 0x0615 ),
130 ( 0x064B, 0x0658 ), ( 0x0670, 0x0670 ), ( 0x06D6, 0x06E4 ),
131 ( 0x06E7, 0x06E8 ), ( 0x06EA, 0x06ED ), ( 0x070F, 0x070F ),
132 ( 0x0711, 0x0711 ), ( 0x0730, 0x074A ), ( 0x07A6, 0x07B0 ),
133 ( 0x0901, 0x0902 ), ( 0x093C, 0x093C ), ( 0x0941, 0x0948 ),
134 ( 0x094D, 0x094D ), ( 0x0951, 0x0954 ), ( 0x0962, 0x0963 ),
135 ( 0x0981, 0x0981 ), ( 0x09BC, 0x09BC ), ( 0x09C1, 0x09C4 ),
136 ( 0x09CD, 0x09CD ), ( 0x09E2, 0x09E3 ), ( 0x0A01, 0x0A02 ),
137 ( 0x0A3C, 0x0A3C ), ( 0x0A41, 0x0A42 ), ( 0x0A47, 0x0A48 ),
138 ( 0x0A4B, 0x0A4D ), ( 0x0A70, 0x0A71 ), ( 0x0A81, 0x0A82 ),
139 ( 0x0ABC, 0x0ABC ), ( 0x0AC1, 0x0AC5 ), ( 0x0AC7, 0x0AC8 ),
140 ( 0x0ACD, 0x0ACD ), ( 0x0AE2, 0x0AE3 ), ( 0x0B01, 0x0B01 ),
141 ( 0x0B3C, 0x0B3C ), ( 0x0B3F, 0x0B3F ), ( 0x0B41, 0x0B43 ),
142 ( 0x0B4D, 0x0B4D ), ( 0x0B56, 0x0B56 ), ( 0x0B82, 0x0B82 ),
143 ( 0x0BC0, 0x0BC0 ), ( 0x0BCD, 0x0BCD ), ( 0x0C3E, 0x0C40 ),
144 ( 0x0C46, 0x0C48 ), ( 0x0C4A, 0x0C4D ), ( 0x0C55, 0x0C56 ),
145 ( 0x0CBC, 0x0CBC ), ( 0x0CBF, 0x0CBF ), ( 0x0CC6, 0x0CC6 ),
146 ( 0x0CCC, 0x0CCD ), ( 0x0D41, 0x0D43 ), ( 0x0D4D, 0x0D4D ),
147 ( 0x0DCA, 0x0DCA ), ( 0x0DD2, 0x0DD4 ), ( 0x0DD6, 0x0DD6 ),
148 ( 0x0E31, 0x0E31 ), ( 0x0E34, 0x0E3A ), ( 0x0E47, 0x0E4E ),
149 ( 0x0EB1, 0x0EB1 ), ( 0x0EB4, 0x0EB9 ), ( 0x0EBB, 0x0EBC ),
150 ( 0x0EC8, 0x0ECD ), ( 0x0F18, 0x0F19 ), ( 0x0F35, 0x0F35 ),
151 ( 0x0F37, 0x0F37 ), ( 0x0F39, 0x0F39 ), ( 0x0F71, 0x0F7E ),
152 ( 0x0F80, 0x0F84 ), ( 0x0F86, 0x0F87 ), ( 0x0F90, 0x0F97 ),
153 ( 0x0F99, 0x0FBC ), ( 0x0FC6, 0x0FC6 ), ( 0x102D, 0x1030 ),
154 ( 0x1032, 0x1032 ), ( 0x1036, 0x1037 ), ( 0x1039, 0x1039 ),
155 ( 0x1058, 0x1059 ), ( 0x1160, 0x11FF ), ( 0x1712, 0x1714 ),
156 ( 0x1732, 0x1734 ), ( 0x1752, 0x1753 ), ( 0x1772, 0x1773 ),
157 ( 0x17B4, 0x17B5 ), ( 0x17B7, 0x17BD ), ( 0x17C6, 0x17C6 ),
158 ( 0x17C9, 0x17D3 ), ( 0x17DD, 0x17DD ), ( 0x180B, 0x180D ),
159 ( 0x18A9, 0x18A9 ), ( 0x1920, 0x1922 ), ( 0x1927, 0x1928 ),
160 ( 0x1932, 0x1932 ), ( 0x1939, 0x193B ), ( 0x200B, 0x200F ),
161 ( 0x202A, 0x202E ), ( 0x2060, 0x2063 ), ( 0x206A, 0x206F ),
162 ( 0x20D0, 0x20EA ), ( 0x302A, 0x302F ), ( 0x3099, 0x309A ),
163 ( 0xFB1E, 0xFB1E ), ( 0xFE00, 0xFE0F ), ( 0xFE20, 0xFE23 ),
164 ( 0xFEFF, 0xFEFF ), ( 0xFFF9, 0xFFFB ), ( 0x1D167, 0x1D169 ),
165 ( 0x1D173, 0x1D182 ), ( 0x1D185, 0x1D18B ), ( 0x1D1AA, 0x1D1AD ),
166 ( 0xE0001, 0xE0001 ), ( 0xE0020, 0xE007F ), ( 0xE0100, 0xE01EF )
167 ]
168
169 def wcwidth(c):
170 """
171 Return the width in character cells of the Unicode character
172 whose code is c
173 """
174
175 ucs = ord(c)
176 # test for 8-bit control characters
177 if ucs == 0:
178 return 0
179 if ucs < 32 or (ucs >= 0x7f and ucs < 0xa0):
180 return -1
181
182 # binary search in table of non-spacing characters
183 if _bisearch(ucs, _combining):
184 return 0
185
186 # if we arrive here, ucs is not a combining or C0/C1 control character
187
188 return 1+ \
189 (ucs >= 0x1100 and
190 (ucs <= 0x115f or # Hangul Jamo init. consonants
191 ucs == 0x2329 or ucs == 0x232a or
192 (ucs >= 0x2e80 and ucs <= 0xa4cf and
193 ucs != 0x303f) or # CJK ... Yi
194 (ucs >= 0xac00 and ucs <= 0xd7a3) or # Hangul Syllables
195 (ucs >= 0xf900 and ucs <= 0xfaff) or # CJK Compatibility Ideographs *
196 (ucs >= 0xfe30 and ucs <= 0xfe6f) or # CJK Compatibility Forms
197 (ucs >= 0xff00 and ucs <= 0xff60) or # Fullwidth Forms
198 (ucs >= 0xffe0 and ucs <= 0xffe6) or
199 (ucs >= 0x20000 and ucs <= 0x2fffd) or
200 (ucs >= 0x30000 and ucs <= 0x3fffd)))
201
202 def wcswidth( pwcs ):
203
204 """
205 Return the width in character cells of the unicode string pwcs,
206 or -1 if the string contains non-printable characters.
207 """
208
209 width = 0
210 for c in pwcs:
211 w = wcwidth(c)
212 if w < 0:
213 return -1
214 else:
215 width += w
216 return width
217
218
219 """
220 sorted list of non-overlapping intervals of East Asian Ambiguous
221 characters, generated by "uniset +WIDTH-A -cat=Me -cat=Mn -cat=Cf c"
222 """
223 _ambiguous = [
224 ( 0x00A1, 0x00A1 ), ( 0x00A4, 0x00A4 ), ( 0x00A7, 0x00A8 ),
225 ( 0x00AA, 0x00AA ), ( 0x00AE, 0x00AE ), ( 0x00B0, 0x00B4 ),
226 ( 0x00B6, 0x00BA ), ( 0x00BC, 0x00BF ), ( 0x00C6, 0x00C6 ),
227 ( 0x00D0, 0x00D0 ), ( 0x00D7, 0x00D8 ), ( 0x00DE, 0x00E1 ),
228 ( 0x00E6, 0x00E6 ), ( 0x00E8, 0x00EA ), ( 0x00EC, 0x00ED ),
229 ( 0x00F0, 0x00F0 ), ( 0x00F2, 0x00F3 ), ( 0x00F7, 0x00FA ),
230 ( 0x00FC, 0x00FC ), ( 0x00FE, 0x00FE ), ( 0x0101, 0x0101 ),
231 ( 0x0111, 0x0111 ), ( 0x0113, 0x0113 ), ( 0x011B, 0x011B ),
232 ( 0x0126, 0x0127 ), ( 0x012B, 0x012B ), ( 0x0131, 0x0133 ),
233 ( 0x0138, 0x0138 ), ( 0x013F, 0x0142 ), ( 0x0144, 0x0144 ),
234 ( 0x0148, 0x014B ), ( 0x014D, 0x014D ), ( 0x0152, 0x0153 ),
235 ( 0x0166, 0x0167 ), ( 0x016B, 0x016B ), ( 0x01CE, 0x01CE ),
236 ( 0x01D0, 0x01D0 ), ( 0x01D2, 0x01D2 ), ( 0x01D4, 0x01D4 ),
237 ( 0x01D6, 0x01D6 ), ( 0x01D8, 0x01D8 ), ( 0x01DA, 0x01DA ),
238 ( 0x01DC, 0x01DC ), ( 0x0251, 0x0251 ), ( 0x0261, 0x0261 ),
239 ( 0x02C4, 0x02C4 ), ( 0x02C7, 0x02C7 ), ( 0x02C9, 0x02CB ),
240 ( 0x02CD, 0x02CD ), ( 0x02D0, 0x02D0 ), ( 0x02D8, 0x02DB ),
241 ( 0x02DD, 0x02DD ), ( 0x02DF, 0x02DF ), ( 0x0391, 0x03A1 ),
242 ( 0x03A3, 0x03A9 ), ( 0x03B1, 0x03C1 ), ( 0x03C3, 0x03C9 ),
243 ( 0x0401, 0x0401 ), ( 0x0410, 0x044F ), ( 0x0451, 0x0451 ),
244 ( 0x2010, 0x2010 ), ( 0x2013, 0x2016 ), ( 0x2018, 0x2019 ),
245 ( 0x201C, 0x201D ), ( 0x2020, 0x2022 ), ( 0x2024, 0x2027 ),
246 ( 0x2030, 0x2030 ), ( 0x2032, 0x2033 ), ( 0x2035, 0x2035 ),
247 ( 0x203B, 0x203B ), ( 0x203E, 0x203E ), ( 0x2074, 0x2074 ),
248 ( 0x207F, 0x207F ), ( 0x2081, 0x2084 ), ( 0x20AC, 0x20AC ),
249 ( 0x2103, 0x2103 ), ( 0x2105, 0x2105 ), ( 0x2109, 0x2109 ),
250 ( 0x2113, 0x2113 ), ( 0x2116, 0x2116 ), ( 0x2121, 0x2122 ),
251 ( 0x2126, 0x2126 ), ( 0x212B, 0x212B ), ( 0x2153, 0x2154 ),
252 ( 0x215B, 0x215E ), ( 0x2160, 0x216B ), ( 0x2170, 0x2179 ),
253 ( 0x2190, 0x2199 ), ( 0x21B8, 0x21B9 ), ( 0x21D2, 0x21D2 ),
254 ( 0x21D4, 0x21D4 ), ( 0x21E7, 0x21E7 ), ( 0x2200, 0x2200 ),
255 ( 0x2202, 0x2203 ), ( 0x2207, 0x2208 ), ( 0x220B, 0x220B ),
256 ( 0x220F, 0x220F ), ( 0x2211, 0x2211 ), ( 0x2215, 0x2215 ),
257 ( 0x221A, 0x221A ), ( 0x221D, 0x2220 ), ( 0x2223, 0x2223 ),
258 ( 0x2225, 0x2225 ), ( 0x2227, 0x222C ), ( 0x222E, 0x222E ),
259 ( 0x2234, 0x2237 ), ( 0x223C, 0x223D ), ( 0x2248, 0x2248 ),
260 ( 0x224C, 0x224C ), ( 0x2252, 0x2252 ), ( 0x2260, 0x2261 ),
261 ( 0x2264, 0x2267 ), ( 0x226A, 0x226B ), ( 0x226E, 0x226F ),
262 ( 0x2282, 0x2283 ), ( 0x2286, 0x2287 ), ( 0x2295, 0x2295 ),
263 ( 0x2299, 0x2299 ), ( 0x22A5, 0x22A5 ), ( 0x22BF, 0x22BF ),
264 ( 0x2312, 0x2312 ), ( 0x2460, 0x24E9 ), ( 0x24EB, 0x254B ),
265 ( 0x2550, 0x2573 ), ( 0x2580, 0x258F ), ( 0x2592, 0x2595 ),
266 ( 0x25A0, 0x25A1 ), ( 0x25A3, 0x25A9 ), ( 0x25B2, 0x25B3 ),
267 ( 0x25B6, 0x25B7 ), ( 0x25BC, 0x25BD ), ( 0x25C0, 0x25C1 ),
268 ( 0x25C6, 0x25C8 ), ( 0x25CB, 0x25CB ), ( 0x25CE, 0x25D1 ),
269 ( 0x25E2, 0x25E5 ), ( 0x25EF, 0x25EF ), ( 0x2605, 0x2606 ),
270 ( 0x2609, 0x2609 ), ( 0x260E, 0x260F ), ( 0x2614, 0x2615 ),
271 ( 0x261C, 0x261C ), ( 0x261E, 0x261E ), ( 0x2640, 0x2640 ),
272 ( 0x2642, 0x2642 ), ( 0x2660, 0x2661 ), ( 0x2663, 0x2665 ),
273 ( 0x2667, 0x266A ), ( 0x266C, 0x266D ), ( 0x266F, 0x266F ),
274 ( 0x273D, 0x273D ), ( 0x2776, 0x277F ), ( 0xE000, 0xF8FF ),
275 ( 0xFFFD, 0xFFFD ), ( 0xF0000, 0xFFFFD ), ( 0x100000, 0x10FFFD )
276 ]
277
278 """
279 * The following functions are the same as mk_wcwidth() and
280 * mk_wcwidth_cjk(), except that spacing characters in the East Asian
281 * Ambiguous (A) category as defined in Unicode Technical Report #11
282 * have a column width of 2. This variant might be useful for users of
283 * CJK legacy encodings who want to migrate to UCS without changing
284 * the traditional terminal character-width behaviour. It is not
285 * otherwise recommended for general use.
286 """
287
288 def wcwidth_cjk(ucs):
289 """ As wcwidth above, but spacing characters in the East Asian
290 Ambiguous (A) category as defined in Unicode Technical Report #11
291 have a column width of 2.
292 """
293 if _bisearch(ucs, _ambiguous):
294 return 2
295 else:
296 return wcwidth(ucs)
297
298 def wcswidth_cjk(pwcs):
299 """ As wcswidth above, but spacing characters in the East Asian
300 Ambiguous (A) category as defined in Unicode Technical Report #11
301 have a column width of 2.
302 """
303 width = 0
304 for c in pwcs:
305 w = wcwidth_cjk(c)
306 if w < 0:
307 return -1
308 else:
309 width += w
310 return width
311
312 #####################################################################
313
314 def _measure_string(ucs, length):
315 t = 0
316 i = 0
317 while t < length and i < len(ucs):
318 t += wcswidth(ucs[i])
319 i += 1
320 return (ucs[:i], t)
321
322 def rpadstring(ucs, length, padchar=' '):
323 """ Right-pad a Unicode string with padchar so that its width in
324 character cells is length. Padchar must be of width 1. The string
325 is truncated if it is too long."""
326
327 s, t = _measure_string(ucs,length)
328
329 if t > length:
330 return s[:-1] + padchar
331 elif t < length:
332 return s + padchar * (length-t)
333 else:
334 return s
335
336 def truncatestring(ucs, length):
337 """ Truncate a Unicode string so that its length is as long as it
338 can be without exceeding length."""
339
340 s, t = _measure_string(ucs,length)
341
342 if t > length:
343 return s[:-1]
344 else:
345 return s
346
347 def wcWidth(c):
348 return wcwidth(chr(c))
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 """
7 * This is an implementation of wcwidth() and wcswidth() (defined in
8 * IEEE Std 1002.1-2001) for Unicode.
9 *
10 * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html
11 * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html
12 *
13 * In fixed-width output devices, Latin characters all occupy a single
14 * "cell" position of equal width, whereas ideographic CJK characters
15 * occupy two such cells. Interoperability between terminal-line
16 * applications and (teletype-style) character terminals using the
17 * UTF-8 encoding requires agreement on which character should advance
18 * the cursor by how many cell positions. No established formal
19 * standards exist at present on which Unicode character shall occupy
20 * how many cell positions on character terminals. These routines are
21 * a first attempt of defining such behavior based on simple rules
22 * applied to data provided by the Unicode Consortium.
23 *
24 * For some graphical characters, the Unicode standard explicitly
25 * defines a character-cell width via the definition of the East Asian
26 * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes.
27 * In all these cases, there is no ambiguity about which width a
28 * terminal shall use. For characters in the East Asian Ambiguous (A)
29 * class, the width choice depends purely on a preference of backward
30 * compatibility with either historic CJK or Western practice.
31 * Choosing single-width for these characters is easy to justify as
32 * the appropriate long-term solution, as the CJK practice of
33 * displaying these characters as double-width comes from historic
34 * implementation simplicity (8-bit encoded characters were displayed
35 * single-width and 16-bit ones double-width, even for Greek,
36 * Cyrillic, etc.) and not any typographic considerations.
37 *
38 * Much less clear is the choice of width for the Not East Asian
39 * (Neutral) class. Existing practice does not dictate a width for any
40 * of these characters. It would nevertheless make sense
41 * typographically to allocate two character cells to characters such
42 * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be
43 * represented adequately with a single-width glyph. The following
44 * routines at present merely assign a single-cell width to all
45 * neutral characters, in the interest of simplicity. This is not
46 * entirely satisfactory and should be reconsidered before
47 * establishing a formal standard in this area. At the moment, the
48 * decision which Not East Asian (Neutral) characters should be
49 * represented by double-width glyphs cannot yet be answered by
50 * applying a simple rule from the Unicode database content. Setting
51 * up a proper standard for the behavior of UTF-8 character terminals
52 * will require a careful analysis not only of each Unicode character,
53 * but also of each presentation form, something the author of these
54 * routines has avoided to do so far.
55 *
56 * http://www.unicode.org/unicode/reports/tr11/
57 *
58 * Markus Kuhn -- 2003-05-20 (Unicode 4.0)
59 *
60 * Permission to use, copy, modify, and distribute this software
61 * for any purpose and without fee is hereby granted. The author
62 * disclaims all warranties with regard to this software.
63 *
64 * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
65
66 """
67
68
69 # auxiliary function for binary search in interval table
70 def _bisearch(ucs, table):
71 min = 0
72 max = len(table)-1
73
74 if ucs < table[0][0] or ucs > table[max][1]:
75 return 0
76 while max >= min:
77 mid = (min + max) / 2
78 if ucs > table[mid][1]:
79 min = mid + 1
80 elif ucs < table[mid][0]:
81 max = mid - 1
82 else:
83 return 1
84
85 return 0
86
87 """
88 * The following two functions define the column width of an ISO 10646
89 * character as follows:
90 *
91 * - The null character (U+0000) has a column width of 0.
92 *
93 * - Other C0/C1 control characters and DEL will lead to a return
94 * value of -1.
95 *
96 * - Non-spacing and enclosing combining characters (general
97 * category code Mn or Me in the Unicode database) have a
98 * column width of 0.
99 *
100 * - SOFT HYPHEN (U+00AD) has a column width of 1.
101 *
102 * - Other format characters (general category code Cf in the Unicode
103 * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
104 *
105 * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
106 * have a column width of 0.
107 *
108 * - Spacing characters in the East Asian Wide (W) or East Asian
109 * Full-width (F) category as defined in Unicode Technical
110 * Report #11 have a column width of 2.
111 *
112 * - All remaining characters (including all printable
113 * ISO 8859-1 and WGL4 characters, Unicode control characters,
114 * etc.) have a column width of 1.
115 *
116 * This implementation assumes that wchar_t characters are encoded
117 * in ISO 10646.
118 """
119
120 """
121 sorted list of non-overlapping intervals of non-spacing characters
122 generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c"
123 """
124
125 _combining = [
126 ( 0x0300, 0x0357 ), ( 0x035D, 0x036F ), ( 0x0483, 0x0486 ),
127 ( 0x0488, 0x0489 ), ( 0x0591, 0x05A1 ), ( 0x05A3, 0x05B9 ),
128 ( 0x05BB, 0x05BD ), ( 0x05BF, 0x05BF ), ( 0x05C1, 0x05C2 ),
129 ( 0x05C4, 0x05C4 ), ( 0x0600, 0x0603 ), ( 0x0610, 0x0615 ),
130 ( 0x064B, 0x0658 ), ( 0x0670, 0x0670 ), ( 0x06D6, 0x06E4 ),
131 ( 0x06E7, 0x06E8 ), ( 0x06EA, 0x06ED ), ( 0x070F, 0x070F ),
132 ( 0x0711, 0x0711 ), ( 0x0730, 0x074A ), ( 0x07A6, 0x07B0 ),
133 ( 0x0901, 0x0902 ), ( 0x093C, 0x093C ), ( 0x0941, 0x0948 ),
134 ( 0x094D, 0x094D ), ( 0x0951, 0x0954 ), ( 0x0962, 0x0963 ),
135 ( 0x0981, 0x0981 ), ( 0x09BC, 0x09BC ), ( 0x09C1, 0x09C4 ),
136 ( 0x09CD, 0x09CD ), ( 0x09E2, 0x09E3 ), ( 0x0A01, 0x0A02 ),
137 ( 0x0A3C, 0x0A3C ), ( 0x0A41, 0x0A42 ), ( 0x0A47, 0x0A48 ),
138 ( 0x0A4B, 0x0A4D ), ( 0x0A70, 0x0A71 ), ( 0x0A81, 0x0A82 ),
139 ( 0x0ABC, 0x0ABC ), ( 0x0AC1, 0x0AC5 ), ( 0x0AC7, 0x0AC8 ),
140 ( 0x0ACD, 0x0ACD ), ( 0x0AE2, 0x0AE3 ), ( 0x0B01, 0x0B01 ),
141 ( 0x0B3C, 0x0B3C ), ( 0x0B3F, 0x0B3F ), ( 0x0B41, 0x0B43 ),
142 ( 0x0B4D, 0x0B4D ), ( 0x0B56, 0x0B56 ), ( 0x0B82, 0x0B82 ),
143 ( 0x0BC0, 0x0BC0 ), ( 0x0BCD, 0x0BCD ), ( 0x0C3E, 0x0C40 ),
144 ( 0x0C46, 0x0C48 ), ( 0x0C4A, 0x0C4D ), ( 0x0C55, 0x0C56 ),
145 ( 0x0CBC, 0x0CBC ), ( 0x0CBF, 0x0CBF ), ( 0x0CC6, 0x0CC6 ),
146 ( 0x0CCC, 0x0CCD ), ( 0x0D41, 0x0D43 ), ( 0x0D4D, 0x0D4D ),
147 ( 0x0DCA, 0x0DCA ), ( 0x0DD2, 0x0DD4 ), ( 0x0DD6, 0x0DD6 ),
148 ( 0x0E31, 0x0E31 ), ( 0x0E34, 0x0E3A ), ( 0x0E47, 0x0E4E ),
149 ( 0x0EB1, 0x0EB1 ), ( 0x0EB4, 0x0EB9 ), ( 0x0EBB, 0x0EBC ),
150 ( 0x0EC8, 0x0ECD ), ( 0x0F18, 0x0F19 ), ( 0x0F35, 0x0F35 ),
151 ( 0x0F37, 0x0F37 ), ( 0x0F39, 0x0F39 ), ( 0x0F71, 0x0F7E ),
152 ( 0x0F80, 0x0F84 ), ( 0x0F86, 0x0F87 ), ( 0x0F90, 0x0F97 ),
153 ( 0x0F99, 0x0FBC ), ( 0x0FC6, 0x0FC6 ), ( 0x102D, 0x1030 ),
154 ( 0x1032, 0x1032 ), ( 0x1036, 0x1037 ), ( 0x1039, 0x1039 ),
155 ( 0x1058, 0x1059 ), ( 0x1160, 0x11FF ), ( 0x1712, 0x1714 ),
156 ( 0x1732, 0x1734 ), ( 0x1752, 0x1753 ), ( 0x1772, 0x1773 ),
157 ( 0x17B4, 0x17B5 ), ( 0x17B7, 0x17BD ), ( 0x17C6, 0x17C6 ),
158 ( 0x17C9, 0x17D3 ), ( 0x17DD, 0x17DD ), ( 0x180B, 0x180D ),
159 ( 0x18A9, 0x18A9 ), ( 0x1920, 0x1922 ), ( 0x1927, 0x1928 ),
160 ( 0x1932, 0x1932 ), ( 0x1939, 0x193B ), ( 0x200B, 0x200F ),
161 ( 0x202A, 0x202E ), ( 0x2060, 0x2063 ), ( 0x206A, 0x206F ),
162 ( 0x20D0, 0x20EA ), ( 0x302A, 0x302F ), ( 0x3099, 0x309A ),
163 ( 0xFB1E, 0xFB1E ), ( 0xFE00, 0xFE0F ), ( 0xFE20, 0xFE23 ),
164 ( 0xFEFF, 0xFEFF ), ( 0xFFF9, 0xFFFB ), ( 0x1D167, 0x1D169 ),
165 ( 0x1D173, 0x1D182 ), ( 0x1D185, 0x1D18B ), ( 0x1D1AA, 0x1D1AD ),
166 ( 0xE0001, 0xE0001 ), ( 0xE0020, 0xE007F ), ( 0xE0100, 0xE01EF )
167 ]
168
169 def wcwidth(c):
170 """
171 Return the width in character cells of the Unicode character
172 whose code is c
173 """
174
175 ucs = ord(c)
176 # test for 8-bit control characters
177 if ucs == 0:
178 return 0
179 if ucs < 32 or (ucs >= 0x7f and ucs < 0xa0):
180 return -1
181
182 # binary search in table of non-spacing characters
183 if _bisearch(ucs, _combining):
184 return 0
185
186 # if we arrive here, ucs is not a combining or C0/C1 control character
187
188 return 1+ \
189 (ucs >= 0x1100 and
190 (ucs <= 0x115f or # Hangul Jamo init. consonants
191 ucs == 0x2329 or ucs == 0x232a or
192 (ucs >= 0x2e80 and ucs <= 0xa4cf and
193 ucs != 0x303f) or # CJK ... Yi
194 (ucs >= 0xac00 and ucs <= 0xd7a3) or # Hangul Syllables
195 (ucs >= 0xf900 and ucs <= 0xfaff) or # CJK Compatibility Ideographs *
196 (ucs >= 0xfe30 and ucs <= 0xfe6f) or # CJK Compatibility Forms
197 (ucs >= 0xff00 and ucs <= 0xff60) or # Fullwidth Forms
198 (ucs >= 0xffe0 and ucs <= 0xffe6) or
199 (ucs >= 0x20000 and ucs <= 0x2fffd) or
200 (ucs >= 0x30000 and ucs <= 0x3fffd)))
201
202 def wcswidth( pwcs ):
203
204 """
205 Return the width in character cells of the unicode string pwcs,
206 or -1 if the string contains non-printable characters.
207 """
208
209 width = 0
210 for c in pwcs:
211 w = wcwidth(c)
212 if w < 0:
213 return -1
214 else:
215 width += w
216 return width
217
218
219 """
220 sorted list of non-overlapping intervals of East Asian Ambiguous
221 characters, generated by "uniset +WIDTH-A -cat=Me -cat=Mn -cat=Cf c"
222 """
223 _ambiguous = [
224 ( 0x00A1, 0x00A1 ), ( 0x00A4, 0x00A4 ), ( 0x00A7, 0x00A8 ),
225 ( 0x00AA, 0x00AA ), ( 0x00AE, 0x00AE ), ( 0x00B0, 0x00B4 ),
226 ( 0x00B6, 0x00BA ), ( 0x00BC, 0x00BF ), ( 0x00C6, 0x00C6 ),
227 ( 0x00D0, 0x00D0 ), ( 0x00D7, 0x00D8 ), ( 0x00DE, 0x00E1 ),
228 ( 0x00E6, 0x00E6 ), ( 0x00E8, 0x00EA ), ( 0x00EC, 0x00ED ),
229 ( 0x00F0, 0x00F0 ), ( 0x00F2, 0x00F3 ), ( 0x00F7, 0x00FA ),
230 ( 0x00FC, 0x00FC ), ( 0x00FE, 0x00FE ), ( 0x0101, 0x0101 ),
231 ( 0x0111, 0x0111 ), ( 0x0113, 0x0113 ), ( 0x011B, 0x011B ),
232 ( 0x0126, 0x0127 ), ( 0x012B, 0x012B ), ( 0x0131, 0x0133 ),
233 ( 0x0138, 0x0138 ), ( 0x013F, 0x0142 ), ( 0x0144, 0x0144 ),
234 ( 0x0148, 0x014B ), ( 0x014D, 0x014D ), ( 0x0152, 0x0153 ),
235 ( 0x0166, 0x0167 ), ( 0x016B, 0x016B ), ( 0x01CE, 0x01CE ),
236 ( 0x01D0, 0x01D0 ), ( 0x01D2, 0x01D2 ), ( 0x01D4, 0x01D4 ),
237 ( 0x01D6, 0x01D6 ), ( 0x01D8, 0x01D8 ), ( 0x01DA, 0x01DA ),
238 ( 0x01DC, 0x01DC ), ( 0x0251, 0x0251 ), ( 0x0261, 0x0261 ),
239 ( 0x02C4, 0x02C4 ), ( 0x02C7, 0x02C7 ), ( 0x02C9, 0x02CB ),
240 ( 0x02CD, 0x02CD ), ( 0x02D0, 0x02D0 ), ( 0x02D8, 0x02DB ),
241 ( 0x02DD, 0x02DD ), ( 0x02DF, 0x02DF ), ( 0x0391, 0x03A1 ),
242 ( 0x03A3, 0x03A9 ), ( 0x03B1, 0x03C1 ), ( 0x03C3, 0x03C9 ),
243 ( 0x0401, 0x0401 ), ( 0x0410, 0x044F ), ( 0x0451, 0x0451 ),
244 ( 0x2010, 0x2010 ), ( 0x2013, 0x2016 ), ( 0x2018, 0x2019 ),
245 ( 0x201C, 0x201D ), ( 0x2020, 0x2022 ), ( 0x2024, 0x2027 ),
246 ( 0x2030, 0x2030 ), ( 0x2032, 0x2033 ), ( 0x2035, 0x2035 ),
247 ( 0x203B, 0x203B ), ( 0x203E, 0x203E ), ( 0x2074, 0x2074 ),
248 ( 0x207F, 0x207F ), ( 0x2081, 0x2084 ), ( 0x20AC, 0x20AC ),
249 ( 0x2103, 0x2103 ), ( 0x2105, 0x2105 ), ( 0x2109, 0x2109 ),
250 ( 0x2113, 0x2113 ), ( 0x2116, 0x2116 ), ( 0x2121, 0x2122 ),
251 ( 0x2126, 0x2126 ), ( 0x212B, 0x212B ), ( 0x2153, 0x2154 ),
252 ( 0x215B, 0x215E ), ( 0x2160, 0x216B ), ( 0x2170, 0x2179 ),
253 ( 0x2190, 0x2199 ), ( 0x21B8, 0x21B9 ), ( 0x21D2, 0x21D2 ),
254 ( 0x21D4, 0x21D4 ), ( 0x21E7, 0x21E7 ), ( 0x2200, 0x2200 ),
255 ( 0x2202, 0x2203 ), ( 0x2207, 0x2208 ), ( 0x220B, 0x220B ),
256 ( 0x220F, 0x220F ), ( 0x2211, 0x2211 ), ( 0x2215, 0x2215 ),
257 ( 0x221A, 0x221A ), ( 0x221D, 0x2220 ), ( 0x2223, 0x2223 ),
258 ( 0x2225, 0x2225 ), ( 0x2227, 0x222C ), ( 0x222E, 0x222E ),
259 ( 0x2234, 0x2237 ), ( 0x223C, 0x223D ), ( 0x2248, 0x2248 ),
260 ( 0x224C, 0x224C ), ( 0x2252, 0x2252 ), ( 0x2260, 0x2261 ),
261 ( 0x2264, 0x2267 ), ( 0x226A, 0x226B ), ( 0x226E, 0x226F ),
262 ( 0x2282, 0x2283 ), ( 0x2286, 0x2287 ), ( 0x2295, 0x2295 ),
263 ( 0x2299, 0x2299 ), ( 0x22A5, 0x22A5 ), ( 0x22BF, 0x22BF ),
264 ( 0x2312, 0x2312 ), ( 0x2460, 0x24E9 ), ( 0x24EB, 0x254B ),
265 ( 0x2550, 0x2573 ), ( 0x2580, 0x258F ), ( 0x2592, 0x2595 ),
266 ( 0x25A0, 0x25A1 ), ( 0x25A3, 0x25A9 ), ( 0x25B2, 0x25B3 ),
267 ( 0x25B6, 0x25B7 ), ( 0x25BC, 0x25BD ), ( 0x25C0, 0x25C1 ),
268 ( 0x25C6, 0x25C8 ), ( 0x25CB, 0x25CB ), ( 0x25CE, 0x25D1 ),
269 ( 0x25E2, 0x25E5 ), ( 0x25EF, 0x25EF ), ( 0x2605, 0x2606 ),
270 ( 0x2609, 0x2609 ), ( 0x260E, 0x260F ), ( 0x2614, 0x2615 ),
271 ( 0x261C, 0x261C ), ( 0x261E, 0x261E ), ( 0x2640, 0x2640 ),
272 ( 0x2642, 0x2642 ), ( 0x2660, 0x2661 ), ( 0x2663, 0x2665 ),
273 ( 0x2667, 0x266A ), ( 0x266C, 0x266D ), ( 0x266F, 0x266F ),
274 ( 0x273D, 0x273D ), ( 0x2776, 0x277F ), ( 0xE000, 0xF8FF ),
275 ( 0xFFFD, 0xFFFD ), ( 0xF0000, 0xFFFFD ), ( 0x100000, 0x10FFFD )
276 ]
277
278 """
279 * The following functions are the same as mk_wcwidth() and
280 * mk_wcwidth_cjk(), except that spacing characters in the East Asian
281 * Ambiguous (A) category as defined in Unicode Technical Report #11
282 * have a column width of 2. This variant might be useful for users of
283 * CJK legacy encodings who want to migrate to UCS without changing
284 * the traditional terminal character-width behaviour. It is not
285 * otherwise recommended for general use.
286 """
287
288 def wcwidth_cjk(ucs):
289 """ As wcwidth above, but spacing characters in the East Asian
290 Ambiguous (A) category as defined in Unicode Technical Report #11
291 have a column width of 2.
292 """
293 if _bisearch(ucs, _ambiguous):
294 return 2
295 else:
296 return wcwidth(ucs)
297
298 def wcswidth_cjk(pwcs):
299 """ As wcswidth above, but spacing characters in the East Asian
300 Ambiguous (A) category as defined in Unicode Technical Report #11
301 have a column width of 2.
302 """
303 width = 0
304 for c in pwcs:
305 w = wcwidth_cjk(c)
306 if w < 0:
307 return -1
308 else:
309 width += w
310 return width
311
312 #####################################################################
313
314 def _measure_string(ucs, length):
315 t = 0
316 i = 0
317 while t < length and i < len(ucs):
318 t += wcswidth(ucs[i])
319 i += 1
320 return (ucs[:i], t)
321
322 def rpadstring(ucs, length, padchar=' '):
323 """ Right-pad a Unicode string with padchar so that its width in
324 character cells is length. Padchar must be of width 1. The string
325 is truncated if it is too long."""
326
327 s, t = _measure_string(ucs,length)
328
329 if t > length:
330 return s[:-1] + padchar
331 elif t < length:
332 return s + padchar * (length-t)
333 else:
334 return s
335
336 def truncatestring(ucs, length):
337 """ Truncate a Unicode string so that its length is as long as it
338 can be without exceeding length."""
339
340 s, t = _measure_string(ucs,length)
341
342 if t > length:
343 return s[:-1]
344 else:
345 return s
346
347 def wcWidth(c):
348 return wcwidth(chr(c))
1212 import managers.mapper_manager
1313 from mockito import mock
1414 from persistence.mappers.abstract_mapper import NullPersistenceManager
15 from model.hosts import Host
15 from model.hosts import Host, ModelObjectVuln
1616 from model.diff import ModelObjectDiff
1717
1818 import test_cases.common as test_utils
4949 diff = ModelObjectDiff(h1, h2)
5050
5151 self.assertTrue(diff.existDiff())
52
53 def test_diff_between_equal_vulns_with_different_confirmed(self):
54 v1 = ModelObjectVuln(name="vuln1",
55 desc="description",
56 severity="high",
57 confirmed=True)
58 v2 = ModelObjectVuln(name="vuln1",
59 desc="description", severity="high")
60
61 self.assertFalse(v1.addUpdate(v2),
62 "The conflict should be resolved automatically")
63 self.assertTrue(v1.confirmed,
64 "The vuln should be still confirmed")
5265
5366
5467 class UpdatesTests(unittest.TestCase):
2323 '../views/reports/_attachments/script/ZeroClipboard.min.js',
2424 '../views/reports/_attachments/script/mousetrap.js',
2525 '../views/reports/_attachments/script/angular-hotkeys.js',
26 '../views/reports/_attachments/script/cryptojs-sha1.js'
26 '../views/reports/_attachments/script/cryptojs-sha1.js',
27 '../views/reports/_attachments/script/Chart.js',
28 '../views/reports/_attachments/script/angular-chart.min.js'
2729 ],
2830
2931 autoWatch : true,
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 """
7 This module will help us to retrieve information
8 about the app state and system information and
9 report it to developers to be able to get information about
10 a crash or bug
11 """
12 import sys
13 import traceback
14 import threading
15 import model.guiapi
16 from cStringIO import StringIO
17 from gui.customevents import ShowExceptionCustomEvent
18 from gui.customevents import EXCEPTION_ID
19 from config.configuration import getInstanceConfiguration
20 import json
21 import time
22
23 CONF = getInstanceConfiguration()
24
25
26
27 def get_crash_log():
28 pass
29
30 def get_system_info():
31 pass
32
33
34 def exception_handler(type, value, tb):
35 """
36 This is a custom exception handler to replace the python original one.
37 The idea is to show the user a dialog with the information and let him/her
38 decide wether to send the developers a report with additional info.
39 The report is created and sent using the callback.
40 Since this handler may be called from threads, the dialog must be created
41 using qt custom events to avoid issues.
42 """
43 import requests
44 import hashlib
45 import platform
46 from pip.commands import freeze
47
48 text = StringIO()
49 traceback.print_exception(type, value, tb, file=text)
50
51
52
53
54 excepts = """
55 Traceback: %s
56 """ % (text.getvalue() )
57
58 exception_hash = hashlib.sha256(excepts).hexdigest()
59 os_dist = " ".join(platform.dist())
60 python_version = platform.python_version()
61 modules_info = ",".join([ "%s=%s" % (x.key, x.version)
62 for x in freeze.get_installed_distributions()])
63
64 python_dist = "Python %s \n Modules: [ %s ]" % (python_version, modules_info)
65
66 description = """
67 Exception: %s
68 Identifier: %s
69 Versions: OS: %s,
70 Python Versions: %s
71 """ % (excepts, exception_hash, os_dist, python_dist)
72
73
74
75 event = ShowExceptionCustomEvent(description, reportToDevelopers)
76 model.guiapi.postCustomEvent(event)
77 text.seek(0)
78 text.truncate()
79 del text
80
81
82
83
84
85
86
87 def reportToDevelopers(self, *description):
88 try:
89 import requests
90 import hashlib
91 import platform
92 from pip.commands import freeze
93
94 uri = CONF.getTktPostUri()
95 headers = json.loads(CONF.getApiParams())
96 params = json.loads(CONF.getApiParams())
97
98 params['description'] = description[0]
99 params['summary'] = 'autoreport %s' % time.time()
100
101 resp = requests.post(uri,
102 headers = headers,
103 data = params, timeout = 1, verify=True)
104 model.api.devlog("Report sent it to faraday server")
105 except Exception as e:
106 model.api.devlog("Error reporting to developers:")
107 model.api.devlog(e)
108
109 def installThreadExcepthook():
110 """
111 Workaround for sys.excepthook thread bug from
112 http://spyced.blogspot.com/2007/06/workaround-for-sysexcepthook-bug.html
113 (https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
114 Call once from __main__ before creating any threads.
115 If using psyco, call psyco.cannotcompile(threading.Thread.run)
116 since this replaces a new-style class method.
117 """
118 init_old = threading.Thread.__init__
119 def init(self, *args, **kwargs):
120 init_old(self, *args, **kwargs)
121 run_old = self.run
122 def run_with_except_hook(*args, **kw):
123 try:
124 run_old(*args, **kw)
125 except (KeyboardInterrupt, SystemExit):
126 raise
127 except Exception:
128 sys.excepthook(*sys.exc_info())
129 self.run = run_with_except_hook
130 threading.Thread.__init__ = init
0 '''
1 Faraday Penetration Test IDE
2 Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/)
3 See the file 'doc/LICENSE' for the license information
4
5 '''
6 """
7 This module will help us to retrieve information
8 about the app state and system information and
9 report it to developers to be able to get information about
10 a crash or bug
11 """
12 import sys
13 import traceback
14 import threading
15 import model.guiapi
16 from cStringIO import StringIO
17 from gui.customevents import ShowExceptionCustomEvent
18 from gui.customevents import EXCEPTION_ID
19 from config.configuration import getInstanceConfiguration
20 import json
21 import time
22
23 CONF = getInstanceConfiguration()
24
25
26
27 def get_crash_log():
28 pass
29
30 def get_system_info():
31 pass
32
33
34 def exception_handler(type, value, tb):
35 """
36 This is a custom exception handler to replace the python original one.
37 The idea is to show the user a dialog with the information and let him/her
38 decide wether to send the developers a report with additional info.
39 The report is created and sent using the callback.
40 Since this handler may be called from threads, the dialog must be created
41 using qt custom events to avoid issues.
42 """
43 import requests
44 import hashlib
45 import platform
46 import pip
47
48 text = StringIO()
49 traceback.print_exception(type, value, tb, file=text)
50
51
52
53
54 excepts = """
55 Traceback: %s
56 """ % (text.getvalue() )
57
58 exception_hash = hashlib.sha256(excepts).hexdigest()
59 os_dist = " ".join(platform.dist())
60 python_version = platform.python_version()
61 modules_info = ",".join([ "%s=%s" % (x.key, x.version)
62 for x in pip.get_installed_distributions()])
63
64 python_dist = "Python %s \n Modules: [ %s ]" % (python_version, modules_info)
65
66 description = """
67 Exception: %s
68 Identifier: %s
69 Versions: OS: %s,
70 Python Versions: %s
71 """ % (excepts, exception_hash, os_dist, python_dist)
72
73
74
75 event = ShowExceptionCustomEvent(description, reportToDevelopers)
76 model.guiapi.postCustomEvent(event)
77 text.seek(0)
78 text.truncate()
79 del text
80
81
82
83
84
85
86
87 def reportToDevelopers(self, *description):
88 try:
89 import requests
90 import hashlib
91 import platform
92 import pip
93
94 uri = CONF.getTktPostUri()
95 headers = json.loads(CONF.getApiParams())
96 params = json.loads(CONF.getApiParams())
97
98 params['description'] = description[0]
99 params['summary'] = 'autoreport %s' % time.time()
100
101 resp = requests.post(uri,
102 headers = headers,
103 data = params, timeout = 1, verify=True)
104 model.api.devlog("Report sent it to faraday server")
105 except Exception as e:
106 model.api.devlog("Error reporting to developers:")
107 model.api.devlog(e)
108
109 def installThreadExcepthook():
110 """
111 Workaround for sys.excepthook thread bug from
112 http://spyced.blogspot.com/2007/06/workaround-for-sysexcepthook-bug.html
113 (https://sourceforge.net/tracker/?func=detail&atid=105470&aid=1230540&group_id=5470).
114 Call once from __main__ before creating any threads.
115 If using psyco, call psyco.cannotcompile(threading.Thread.run)
116 since this replaces a new-style class method.
117 """
118 init_old = threading.Thread.__init__
119 def init(self, *args, **kwargs):
120 init_old(self, *args, **kwargs)
121 run_old = self.run
122 def run_with_except_hook(*args, **kw):
123 try:
124 run_old(*args, **kw)
125 except (KeyboardInterrupt, SystemExit):
126 raise
127 except Exception:
128 sys.excepthook(*sys.exc_info())
129 self.run = run_with_except_hook
130 threading.Thread.__init__ = init
99 import logging
1010 import logging.handlers
1111 import os
12 from config.globals import *
12 from config.globals import CONST_FARADAY_HOME_PATH, CONST_FARADAY_LOGS_PATH
1313
1414
1515 FARADAY_USER_HOME = os.path.expanduser(CONST_FARADAY_HOME_PATH)
16 LOG_FILE = os.path.join(FARADAY_USER_HOME, CONST_FARADAY_LOGS_PATH, 'faraday.log')
17
18 # Default logger
19
20 logger = logging.getLogger('faraday')
21 logger.propagate = False
22 logger.setLevel(logging.INFO)
23
24 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
25
26 ch = logging.StreamHandler()
27 ch.setFormatter(formatter)
28 logger.addHandler(ch)
16 LOG_FILE = os.path.join(
17 FARADAY_USER_HOME, CONST_FARADAY_LOGS_PATH, 'faraday.log')
2918
3019
31 def setUpLogger():
32 from config.configuration import getInstanceConfiguration
33 CONF = getInstanceConfiguration()
34 logger = logging.getLogger('faraday')
35
20 def setUpLogger(debug=False):
3621 level = logging.INFO
37 if CONF.getDebugStatus():
22 if debug:
3823 level = logging.DEBUG
3924
25 logger = logging.getLogger('faraday')
26 logger.propagate = False
4027 logger.setLevel(level)
41 fh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=5*1024*1024, backupCount=5)
28
29 formatter = logging.Formatter(
30 '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
31
32 ch = logging.StreamHandler()
33 ch.setFormatter(formatter)
34 logger.addHandler(ch)
35
36 # File logger
37 fh = logging.handlers.RotatingFileHandler(
38 LOG_FILE, maxBytes=5*1024*1024, backupCount=5)
4239 fh.setFormatter(formatter)
4340 logger.addHandler(fh)
4441
4542
46 def getLogger(obj):
43 def addHandler(handler):
44 logger = logging.getLogger('faraday')
45 logger.addHandler(handler)
46
47
48 def getLogger(obj=None):
4749 """Creates a logger named by a string or an object's class name.
48 Allowing logger to additionally accept strings as names for non-class loggings.
50 Allowing logger to additionally accept strings as names
51 for non-class loggings.
4952 """
50 if type(obj) is str:
51 logger = logging.getLogger('%s.%s' % ('faraday', obj))
53 if obj is None:
54 logger = logging.getLogger(
55 'faraday')
56 elif type(obj) is str:
57 logger = logging.getLogger(
58 '%s.%s' % ('faraday', obj))
5259 else:
53 logger = logging.getLogger('%s.%s' % ('faraday', obj.__class__.__name__))
60 logger = logging.getLogger(
61 '%s.%s' % ('faraday', obj.__class__.__name__))
5462 return logger
717717 justify-content: center;
718718 align-items: center;
719719 }
720 #merge, #delete, #new, #tags, left{margin:5px;float:right}
720 #merge, #delete, #new, #tags, #group-by, left{margin:5px;float:right}
721721 .left_auths{margin:5px;float:right;margin-top: -52px;}
722722 .left_auth{margin:5px;float:right;margin-top: -40px;}
723723
983983 font-size: 15px;
984984 font-weight: bold;
985985 }
986 .text-center{text-align: center; width: 25px;}
986 .text-center{text-align: center; line-height: 40px;}
987987 .severities .color-unclassified, .severities .color-unclassified:hover{color: #999;}
988988 .severities .color-info, .severities .color-info:hover{color: #2e97bd;}
989989 .severities .color-low, .severities .color-low:hover{color: #A1CE31;}
10161016 /* Normalize datepicker */
10171017 div.datepicker ul.dropdown-menu{width: 288px;}
10181018 div.datepicker > p > ul > li > div > table{width: 97%}
1019 div.datepicker .btn-sm, .btn-group-sm > .btn{padding: 5px 7px}
1019 div.datepicker .btn-sm, .btn-group-sm > .btn{padding: 5px 7px}
1020 .disabled{color: #919191}
1021 div.btn-group .vulns-filter{left: -2px;}
1022 div.btn-group .dropdown-menu.margin{margin-left: 38px}
1023 /* UI GRID */
1024 .ui-grid-cell.ui-grid-disable-selection.ui-grid-row-header-cell{pointer-events: auto!important;}
1025 .grid {
1026 height: 98vh;
1027 }
1028 .ui-grid-cell-contents.left-rows.text-center.ng-scope{white-space: normal!important; }
1029 .ui-grid .ui-grid-row:nth-child(odd) .ui-grid-cell .ui-grid-cell-contents .crop-text, .ui-grid .ui-grid-row:nth-child(even) .ui-grid-cell .ui-grid-cell-contents .crop-text, .ui-grid-cell-contents{
1030 white-space: pre;
1031 word-wrap: break-word!important;
1032 }
1033 .grid.ui-grid{font-family: 12px!important;}
1034 .ui-grid-cell-contents.row-tooltip.text-center{white-space: normal!important; }
1035 .white-space{white-space: pre!important; word-wrap: break-word;}
1036 .ui-grid-cell-contents.center > .pos-middle.crop-text{top: 2%!important;white-space: inherit!important;}
1037 .overflow-cell{
1038 height: 95px!important;
1039 overflow-y: auto!important;
1040 }
1041 div.ui-grid-header-cell .ui-grid-cell-contents{white-space: normal;}
2525 <link rel="stylesheet" type="text/css" href="styles/font-awesome.css" />
2626 <link rel="stylesheet" type="text/css" href="styles/angular-hotkeys.css" />
2727 <link rel="stylesheet" type="text/css" href="script/angular-chart.css" />
28 <link rel="stylesheet" type="text/css" href="script/ui-grid.css" />
2829
2930 <!-- Icons -->
3031 <link href="favicon.ico" rel="shortcut icon">
4445 <script type="text/javascript" src="script/angular-file-upload-shim.js"></script><!-- compatibility with older browsers -->
4546 <script type="text/javascript" src="script/angular-file-upload.js"></script>
4647 <script type="text/javascript" src="script/ngClip.js"></script>
47 <script type="text/javascript" src="script/ui-bootstrap-tpls-0.11.2.min.js"></script>
48 <script type="text/javascript" src="script/ui-bootstrap-tpls-0.14.1.min.js"></script>
4849 <script type="text/javascript" src="script/jquery.qtip.js"></script>
4950 <script type="text/javascript" src="script/cryptojs-sha1.js"></script>
5051 <script type="text/javascript" src="script/ZeroClipboard.min.js"></script>
52 <script type="text/javascript" src="script/sanitize.js"></script>
5153 <!-- angular chart -->
5254 <script type="text/javascript" src="script/Chart.js"></script>
5355 <script type="text/javascript" src="script/angular-chart.min.js"></script>
56 <!-- ui-grid -->
57 <script type="text/javascript" src="script/ui-grid.js"></script>
5458 </head>
5559
5660 <body>
00 /**
1 * @license AngularJS v1.2.23
2 * (c) 2010-2014 Google, Inc. http://angularjs.org
1 * @license AngularJS v1.4.3
2 * (c) 2010-2015 Google, Inc. http://angularjs.org
33 * License: MIT
44 */
55 (function(window, angular, undefined) {'use strict';
1616 *
1717 * <div doc-module-components="ngCookies"></div>
1818 *
19 * See {@link ngCookies.$cookies `$cookies`} and
20 * {@link ngCookies.$cookieStore `$cookieStore`} for usage.
19 * See {@link ngCookies.$cookies `$cookies`} for usage.
2120 */
2221
2322
2423 angular.module('ngCookies', ['ng']).
2524 /**
26 * @ngdoc service
27 * @name $cookies
28 *
25 * @ngdoc provider
26 * @name $cookiesProvider
2927 * @description
30 * Provides read/write access to browser's cookies.
31 *
32 * Only a simple Object is exposed and by adding or removing properties to/from this object, new
33 * cookies are created/deleted at the end of current $eval.
34 * The object's properties can only be strings.
35 *
36 * Requires the {@link ngCookies `ngCookies`} module to be installed.
37 *
38 * @example
39 *
40 * ```js
41 * angular.module('cookiesExample', ['ngCookies'])
42 * .controller('ExampleController', ['$cookies', function($cookies) {
43 * // Retrieving a cookie
44 * var favoriteCookie = $cookies.myFavorite;
45 * // Setting a cookie
46 * $cookies.myFavorite = 'oatmeal';
47 * }]);
48 * ```
49 */
50 factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) {
51 var cookies = {},
52 lastCookies = {},
53 lastBrowserCookies,
54 runEval = false,
55 copy = angular.copy,
56 isUndefined = angular.isUndefined;
57
58 //creates a poller fn that copies all cookies from the $browser to service & inits the service
59 $browser.addPollFn(function() {
60 var currentCookies = $browser.cookies();
61 if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl
62 lastBrowserCookies = currentCookies;
63 copy(currentCookies, lastCookies);
64 copy(currentCookies, cookies);
65 if (runEval) $rootScope.$apply();
66 }
67 })();
68
69 runEval = true;
70
71 //at the end of each eval, push cookies
72 //TODO: this should happen before the "delayed" watches fire, because if some cookies are not
73 // strings or browser refuses to store some cookies, we update the model in the push fn.
74 $rootScope.$watch(push);
75
76 return cookies;
77
78
79 /**
80 * Pushes all the cookies from the service to the browser and verifies if all cookies were
81 * stored.
82 */
83 function push() {
84 var name,
85 value,
86 browserCookies,
87 updated;
88
89 //delete any cookies deleted in $cookies
90 for (name in lastCookies) {
91 if (isUndefined(cookies[name])) {
92 $browser.cookies(name, undefined);
93 }
94 }
95
96 //update all cookies updated in $cookies
97 for(name in cookies) {
98 value = cookies[name];
99 if (!angular.isString(value)) {
100 value = '' + value;
101 cookies[name] = value;
102 }
103 if (value !== lastCookies[name]) {
104 $browser.cookies(name, value);
105 updated = true;
106 }
107 }
108
109 //verify what was actually stored
110 if (updated){
111 updated = false;
112 browserCookies = $browser.cookies();
113
114 for (name in cookies) {
115 if (cookies[name] !== browserCookies[name]) {
116 //delete or reset all cookies that the browser dropped from $cookies
117 if (isUndefined(browserCookies[name])) {
118 delete cookies[name];
119 } else {
120 cookies[name] = browserCookies[name];
121 }
122 updated = true;
123 }
124 }
125 }
126 }
127 }]).
128
129
130 /**
131 * @ngdoc service
132 * @name $cookieStore
133 * @requires $cookies
134 *
135 * @description
136 * Provides a key-value (string-object) storage, that is backed by session cookies.
137 * Objects put or retrieved from this storage are automatically serialized or
138 * deserialized by angular's toJson/fromJson.
139 *
140 * Requires the {@link ngCookies `ngCookies`} module to be installed.
141 *
142 * @example
143 *
144 * ```js
145 * angular.module('cookieStoreExample', ['ngCookies'])
146 * .controller('ExampleController', ['$cookieStore', function($cookieStore) {
147 * // Put cookie
148 * $cookieStore.put('myFavorite','oatmeal');
149 * // Get cookie
150 * var favoriteCookie = $cookieStore.get('myFavorite');
151 * // Removing a cookie
152 * $cookieStore.remove('myFavorite');
153 * }]);
154 * ```
155 */
156 factory('$cookieStore', ['$cookies', function($cookies) {
157
28 * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service.
29 * */
30 provider('$cookies', [function $CookiesProvider() {
31 /**
32 * @ngdoc property
33 * @name $cookiesProvider#defaults
34 * @description
35 *
36 * Object containing default options to pass when setting cookies.
37 *
38 * The object may have following properties:
39 *
40 * - **path** - `{string}` - The cookie will be available only for this path and its
41 * sub-paths. By default, this would be the URL that appears in your base tag.
42 * - **domain** - `{string}` - The cookie will be available only for this domain and
43 * its sub-domains. For obvious security reasons the user agent will not accept the
44 * cookie if the current domain is not a sub domain or equals to the requested domain.
45 * - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT"
46 * or a Date object indicating the exact date/time this cookie will expire.
47 * - **secure** - `{boolean}` - The cookie will be available only in secured connection.
48 *
49 * Note: by default the address that appears in your `<base>` tag will be used as path.
50 * This is import so that cookies will be visible for all routes in case html5mode is enabled
51 *
52 **/
53 var defaults = this.defaults = {};
54
55 function calcOptions(options) {
56 return options ? angular.extend({}, defaults, options) : defaults;
57 }
58
59 /**
60 * @ngdoc service
61 * @name $cookies
62 *
63 * @description
64 * Provides read/write access to browser's cookies.
65 *
66 * <div class="alert alert-info">
67 * Up until Angular 1.3, `$cookies` exposed properties that represented the
68 * current browser cookie values. In version 1.4, this behavior has changed, and
69 * `$cookies` now provides a standard api of getters, setters etc.
70 * </div>
71 *
72 * Requires the {@link ngCookies `ngCookies`} module to be installed.
73 *
74 * @example
75 *
76 * ```js
77 * angular.module('cookiesExample', ['ngCookies'])
78 * .controller('ExampleController', ['$cookies', function($cookies) {
79 * // Retrieving a cookie
80 * var favoriteCookie = $cookies.get('myFavorite');
81 * // Setting a cookie
82 * $cookies.put('myFavorite', 'oatmeal');
83 * }]);
84 * ```
85 */
86 this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) {
15887 return {
15988 /**
16089 * @ngdoc method
161 * @name $cookieStore#get
90 * @name $cookies#get
16291 *
16392 * @description
16493 * Returns the value of given cookie key
94 *
95 * @param {string} key Id to use for lookup.
96 * @returns {string} Raw cookie value.
97 */
98 get: function(key) {
99 return $$cookieReader()[key];
100 },
101
102 /**
103 * @ngdoc method
104 * @name $cookies#getObject
105 *
106 * @description
107 * Returns the deserialized value of given cookie key
165108 *
166109 * @param {string} key Id to use for lookup.
167110 * @returns {Object} Deserialized cookie value.
168111 */
169 get: function(key) {
170 var value = $cookies[key];
112 getObject: function(key) {
113 var value = this.get(key);
171114 return value ? angular.fromJson(value) : value;
172115 },
173116
174117 /**
175118 * @ngdoc method
176 * @name $cookieStore#put
119 * @name $cookies#getAll
120 *
121 * @description
122 * Returns a key value object with all the cookies
123 *
124 * @returns {Object} All cookies
125 */
126 getAll: function() {
127 return $$cookieReader();
128 },
129
130 /**
131 * @ngdoc method
132 * @name $cookies#put
177133 *
178134 * @description
179135 * Sets a value for given cookie key
136 *
137 * @param {string} key Id for the `value`.
138 * @param {string} value Raw value to be stored.
139 * @param {Object=} options Options object.
140 * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults}
141 */
142 put: function(key, value, options) {
143 $$cookieWriter(key, value, calcOptions(options));
144 },
145
146 /**
147 * @ngdoc method
148 * @name $cookies#putObject
149 *
150 * @description
151 * Serializes and sets a value for given cookie key
180152 *
181153 * @param {string} key Id for the `value`.
182154 * @param {Object} value Value to be stored.
183 */
184 put: function(key, value) {
185 $cookies[key] = angular.toJson(value);
186 },
187
188 /**
189 * @ngdoc method
190 * @name $cookieStore#remove
155 * @param {Object=} options Options object.
156 * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults}
157 */
158 putObject: function(key, value, options) {
159 this.put(key, angular.toJson(value), options);
160 },
161
162 /**
163 * @ngdoc method
164 * @name $cookies#remove
191165 *
192166 * @description
193167 * Remove given cookie
194168 *
195169 * @param {string} key Id of the key-value pair to delete.
196 */
197 remove: function(key) {
198 delete $cookies[key];
170 * @param {Object=} options Options object.
171 * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults}
172 */
173 remove: function(key, options) {
174 $$cookieWriter(key, undefined, calcOptions(options));
199175 }
200176 };
201
202 }]);
177 }];
178 }]);
179
180 angular.module('ngCookies').
181 /**
182 * @ngdoc service
183 * @name $cookieStore
184 * @deprecated
185 * @requires $cookies
186 *
187 * @description
188 * Provides a key-value (string-object) storage, that is backed by session cookies.
189 * Objects put or retrieved from this storage are automatically serialized or
190 * deserialized by angular's toJson/fromJson.
191 *
192 * Requires the {@link ngCookies `ngCookies`} module to be installed.
193 *
194 * <div class="alert alert-danger">
195 * **Note:** The $cookieStore service is **deprecated**.
196 * Please use the {@link ngCookies.$cookies `$cookies`} service instead.
197 * </div>
198 *
199 * @example
200 *
201 * ```js
202 * angular.module('cookieStoreExample', ['ngCookies'])
203 * .controller('ExampleController', ['$cookieStore', function($cookieStore) {
204 * // Put cookie
205 * $cookieStore.put('myFavorite','oatmeal');
206 * // Get cookie
207 * var favoriteCookie = $cookieStore.get('myFavorite');
208 * // Removing a cookie
209 * $cookieStore.remove('myFavorite');
210 * }]);
211 * ```
212 */
213 factory('$cookieStore', ['$cookies', function($cookies) {
214
215 return {
216 /**
217 * @ngdoc method
218 * @name $cookieStore#get
219 *
220 * @description
221 * Returns the value of given cookie key
222 *
223 * @param {string} key Id to use for lookup.
224 * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist.
225 */
226 get: function(key) {
227 return $cookies.getObject(key);
228 },
229
230 /**
231 * @ngdoc method
232 * @name $cookieStore#put
233 *
234 * @description
235 * Sets a value for given cookie key
236 *
237 * @param {string} key Id for the `value`.
238 * @param {Object} value Value to be stored.
239 */
240 put: function(key, value) {
241 $cookies.putObject(key, value);
242 },
243
244 /**
245 * @ngdoc method
246 * @name $cookieStore#remove
247 *
248 * @description
249 * Remove given cookie
250 *
251 * @param {string} key Id of the key-value pair to delete.
252 */
253 remove: function(key) {
254 $cookies.remove(key);
255 }
256 };
257
258 }]);
259
260 /**
261 * @name $$cookieWriter
262 * @requires $document
263 *
264 * @description
265 * This is a private service for writing cookies
266 *
267 * @param {string} name Cookie name
268 * @param {string=} value Cookie value (if undefined, cookie will be deleted)
269 * @param {Object=} options Object with options that need to be stored for the cookie.
270 */
271 function $$CookieWriter($document, $log, $browser) {
272 var cookiePath = $browser.baseHref();
273 var rawDocument = $document[0];
274
275 function buildCookieString(name, value, options) {
276 var path, expires;
277 options = options || {};
278 expires = options.expires;
279 path = angular.isDefined(options.path) ? options.path : cookiePath;
280 if (value === undefined) {
281 expires = 'Thu, 01 Jan 1970 00:00:00 GMT';
282 value = '';
283 }
284 if (angular.isString(expires)) {
285 expires = new Date(expires);
286 }
287
288 var str = encodeURIComponent(name) + '=' + encodeURIComponent(value);
289 str += path ? ';path=' + path : '';
290 str += options.domain ? ';domain=' + options.domain : '';
291 str += expires ? ';expires=' + expires.toUTCString() : '';
292 str += options.secure ? ';secure' : '';
293
294 // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
295 // - 300 cookies
296 // - 20 cookies per unique domain
297 // - 4096 bytes per cookie
298 var cookieLength = str.length + 1;
299 if (cookieLength > 4096) {
300 $log.warn("Cookie '" + name +
301 "' possibly not set or overflowed because it was too large (" +
302 cookieLength + " > 4096 bytes)!");
303 }
304
305 return str;
306 }
307
308 return function(name, value, options) {
309 rawDocument.cookie = buildCookieString(name, value, options);
310 };
311 }
312
313 $$CookieWriter.$inject = ['$document', '$log', '$browser'];
314
315 angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() {
316 this.$get = $$CookieWriter;
317 });
203318
204319
205320 })(window, window.angular);
00 /**
1 * @license AngularJS v1.2.23
2 * (c) 2010-2014 Google, Inc. http://angularjs.org
1 * @license AngularJS v1.4.3
2 * (c) 2010-2015 Google, Inc. http://angularjs.org
33 * License: MIT
44 */
55 (function(window, angular, undefined) {
5252 self.onUrlChange = function(listener) {
5353 self.pollFns.push(
5454 function() {
55 if (self.$$lastUrl != self.$$url) {
55 if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) {
5656 self.$$lastUrl = self.$$url;
57 listener(self.$$url);
57 self.$$lastState = self.$$state;
58 listener(self.$$url, self.$$state);
5859 }
5960 }
6061 );
6263 return listener;
6364 };
6465
65 self.cookieHash = {};
66 self.lastCookieHash = {};
66 self.$$applicationDestroyed = angular.noop;
67 self.$$checkUrlChange = angular.noop;
68
6769 self.deferredFns = [];
6870 self.deferredNextId = 0;
6971
7072 self.defer = function(fn, delay) {
7173 delay = delay || 0;
7274 self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
73 self.deferredFns.sort(function(a,b){ return a.time - b.time;});
75 self.deferredFns.sort(function(a, b) { return a.time - b.time;});
7476 return self.deferredNextId++;
7577 };
7678
113115 self.defer.now += delay;
114116 } else {
115117 if (self.deferredFns.length) {
116 self.defer.now = self.deferredFns[self.deferredFns.length-1].time;
118 self.defer.now = self.deferredFns[self.deferredFns.length - 1].time;
117119 } else {
118120 throw new Error('No deferred tasks to be flushed');
119121 }
124126 }
125127 };
126128
127 self.$$baseHref = '';
129 self.$$baseHref = '/';
128130 self.baseHref = function() {
129131 return this.$$baseHref;
130132 };
138140 * run all fns in pollFns
139141 */
140142 poll: function poll() {
141 angular.forEach(this.pollFns, function(pollFn){
143 angular.forEach(this.pollFns, function(pollFn) {
142144 pollFn();
143145 });
144146 },
145147
146 addPollFn: function(pollFn) {
147 this.pollFns.push(pollFn);
148 return pollFn;
149 },
150
151 url: function(url, replace) {
148 url: function(url, replace, state) {
149 if (angular.isUndefined(state)) {
150 state = null;
151 }
152152 if (url) {
153153 this.$$url = url;
154 // Native pushState serializes & copies the object; simulate it.
155 this.$$state = angular.copy(state);
154156 return this;
155157 }
156158
157159 return this.$$url;
158160 },
159161
160 cookies: function(name, value) {
161 if (name) {
162 if (angular.isUndefined(value)) {
163 delete this.cookieHash[name];
164 } else {
165 if (angular.isString(value) && //strings only
166 value.length <= 4096) { //strict cookie storage limits
167 this.cookieHash[name] = value;
168 }
169 }
170 } else {
171 if (!angular.equals(this.cookieHash, this.lastCookieHash)) {
172 this.lastCookieHash = angular.copy(this.cookieHash);
173 this.cookieHash = angular.copy(this.cookieHash);
174 }
175 return this.cookieHash;
176 }
162 state: function() {
163 return this.$$state;
177164 },
178165
179166 notifyWhenNoOutstandingRequests: function(fn) {
188175 *
189176 * @description
190177 * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors
191 * passed into the `$exceptionHandler`.
178 * passed to the `$exceptionHandler`.
192179 */
193180
194181 /**
197184 *
198185 * @description
199186 * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed
200 * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
187 * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
201188 * information.
202189 *
203190 *
237224 *
238225 * @param {string} mode Mode of operation, defaults to `rethrow`.
239226 *
240 * - `rethrow`: If any errors are passed into the handler in tests, it typically
241 * means that there is a bug in the application or test, so this mock will
242 * make these tests fail.
243227 * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
244228 * mode stores an array of errors in `$exceptionHandler.errors`, to allow later
245229 * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
246230 * {@link ngMock.$log#reset reset()}
231 * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
232 * is a bug in the application or test, so this mock will make these tests fail.
233 * For any implementations that expect exceptions to be thrown, the `rethrow` mode
234 * will also maintain a log of thrown errors.
247235 */
248236 this.mode = function(mode) {
249 switch(mode) {
237
238 switch (mode) {
239 case 'log':
250240 case 'rethrow':
251 handler = function(e) {
252 throw e;
253 };
254 break;
255 case 'log':
256241 var errors = [];
257
258242 handler = function(e) {
259243 if (arguments.length == 1) {
260244 errors.push(e);
261245 } else {
262246 errors.push([].slice.call(arguments, 0));
263247 }
248 if (mode === "rethrow") {
249 throw e;
250 }
264251 };
265
266252 handler.errors = errors;
267253 break;
268254 default:
304290 }
305291 };
306292
307 this.$get = function () {
293 this.$get = function() {
308294 var $log = {
309295 log: function() { $log.log.logs.push(concat([], arguments, 0)); },
310296 warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
324310 * @description
325311 * Reset all of the logging arrays to empty.
326312 */
327 $log.reset = function () {
313 $log.reset = function() {
328314 /**
329315 * @ngdoc property
330316 * @name $log#log.logs
331317 *
332318 * @description
333 * Array of messages logged using {@link ngMock.$log#log}.
319 * Array of messages logged using {@link ng.$log#log `log()`}.
334320 *
335321 * @example
336322 * ```js
344330 * @name $log#info.logs
345331 *
346332 * @description
347 * Array of messages logged using {@link ngMock.$log#info}.
333 * Array of messages logged using {@link ng.$log#info `info()`}.
348334 *
349335 * @example
350336 * ```js
358344 * @name $log#warn.logs
359345 *
360346 * @description
361 * Array of messages logged using {@link ngMock.$log#warn}.
347 * Array of messages logged using {@link ng.$log#warn `warn()`}.
362348 *
363349 * @example
364350 * ```js
372358 * @name $log#error.logs
373359 *
374360 * @description
375 * Array of messages logged using {@link ngMock.$log#error}.
361 * Array of messages logged using {@link ng.$log#error `error()`}.
376362 *
377363 * @example
378364 * ```js
386372 * @name $log#debug.logs
387373 *
388374 * @description
389 * Array of messages logged using {@link ngMock.$log#debug}.
375 * Array of messages logged using {@link ng.$log#debug `debug()`}.
390376 *
391377 * @example
392378 * ```js
402388 * @name $log#assertEmpty
403389 *
404390 * @description
405 * Assert that the all of the logging methods have no logged messages. If messages present, an
406 * exception is thrown.
391 * Assert that all of the logging methods have no logged messages. If any messages are present,
392 * an exception is thrown.
407393 */
408394 $log.assertEmpty = function() {
409395 var errors = [];
410396 angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) {
411397 angular.forEach($log[logLevel].logs, function(log) {
412 angular.forEach(log, function (logItem) {
398 angular.forEach(log, function(logItem) {
413399 errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' +
414400 (logItem.stack || ''));
415401 });
416402 });
417403 });
418404 if (errors.length) {
419 errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+
405 errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " +
420406 "an expected log message was not checked and removed:");
421407 errors.push('');
422408 throw new Error(errors.join('\n---------\n'));
446432 * indefinitely.
447433 * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
448434 * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
435 * @param {...*=} Pass additional parameters to the executed function.
449436 * @returns {promise} A promise which will be notified on each iteration.
450437 */
451438 angular.mock.$IntervalProvider = function() {
452 this.$get = ['$rootScope', '$q',
453 function($rootScope, $q) {
439 this.$get = ['$browser', '$rootScope', '$q', '$$q',
440 function($browser, $rootScope, $q, $$q) {
454441 var repeatFns = [],
455442 nextRepeatId = 0,
456443 now = 0;
457444
458445 var $interval = function(fn, delay, count, invokeApply) {
459 var deferred = $q.defer(),
460 promise = deferred.promise,
446 var hasParams = arguments.length > 4,
447 args = hasParams ? Array.prototype.slice.call(arguments, 4) : [],
461448 iteration = 0,
462 skipApply = (angular.isDefined(invokeApply) && !invokeApply);
449 skipApply = (angular.isDefined(invokeApply) && !invokeApply),
450 deferred = (skipApply ? $$q : $q).defer(),
451 promise = deferred.promise;
463452
464453 count = (angular.isDefined(count)) ? count : 0;
465 promise.then(null, null, fn);
454 promise.then(null, null, (!hasParams) ? fn : function() {
455 fn.apply(null, args);
456 });
466457
467458 promise.$$intervalId = nextRepeatId;
468459
482473 }
483474 }
484475
485 if (!skipApply) $rootScope.$apply();
476 if (skipApply) {
477 $browser.defer.flush();
478 } else {
479 $rootScope.$apply();
480 }
486481 }
487482
488483 repeatFns.push({
492487 id: nextRepeatId,
493488 deferred: deferred
494489 });
495 repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
490 repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
496491
497492 nextRepeatId++;
498493 return promise;
508503 * @returns {boolean} Returns `true` if the task was successfully cancelled.
509504 */
510505 $interval.cancel = function(promise) {
511 if(!promise) return false;
506 if (!promise) return false;
512507 var fnIndex;
513508
514509 angular.forEach(repeatFns, function(fn, index) {
541536 var task = repeatFns[0];
542537 task.fn();
543538 task.nextTime += task.delay;
544 repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
539 repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
545540 }
546541 return millis;
547542 };
565560 tzHour = 0,
566561 tzMin = 0;
567562 if (match[9]) {
568 tzHour = int(match[9] + match[10]);
569 tzMin = int(match[9] + match[11]);
570 }
571 date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
572 date.setUTCHours(int(match[4]||0) - tzHour,
573 int(match[5]||0) - tzMin,
574 int(match[6]||0),
575 int(match[7]||0));
563 tzHour = toInt(match[9] + match[10]);
564 tzMin = toInt(match[9] + match[11]);
565 }
566 date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
567 date.setUTCHours(toInt(match[4] || 0) - tzHour,
568 toInt(match[5] || 0) - tzMin,
569 toInt(match[6] || 0),
570 toInt(match[7] || 0));
576571 return date;
577572 }
578573 return string;
579574 }
580575
581 function int(str) {
576 function toInt(str) {
582577 return parseInt(str, 10);
583578 }
584579
589584 num = -num;
590585 }
591586 num = '' + num;
592 while(num.length < digits) num = '0' + num;
593 if (trim)
587 while (num.length < digits) num = '0' + num;
588 if (trim) {
594589 num = num.substr(num.length - digits);
590 }
595591 return neg + num;
596592 }
597593
633629 * ```
634630 *
635631 */
636 angular.mock.TzDate = function (offset, timestamp) {
632 angular.mock.TzDate = function(offset, timestamp) {
637633 var self = new Date(0);
638634 if (angular.isString(timestamp)) {
639635 var tsStr = timestamp;
641637 self.origDate = jsonStringToDate(timestamp);
642638
643639 timestamp = self.origDate.getTime();
644 if (isNaN(timestamp))
640 if (isNaN(timestamp)) {
645641 throw {
646642 name: "Illegal Argument",
647643 message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
648644 };
645 }
649646 } else {
650647 self.origDate = new Date(timestamp);
651648 }
652649
653650 var localOffset = new Date(timestamp).getTimezoneOffset();
654 self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
651 self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60;
655652 self.date = new Date(timestamp + self.offsetDiff);
656653
657654 self.getTime = function() {
773770 };
774771 });
775772
776 $provide.decorator('$animate', function($delegate, $$asyncCallback) {
773 $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF',
774 function($delegate, $timeout, $browser, $$rAF) {
777775 var animate = {
778 queue : [],
779 enabled : $delegate.enabled,
780 triggerCallbacks : function() {
781 $$asyncCallback.flush();
776 queue: [],
777 cancel: $delegate.cancel,
778 enabled: $delegate.enabled,
779 triggerCallbackEvents: function() {
780 $$rAF.flush();
782781 },
783 triggerReflow : function() {
782 triggerCallbackPromise: function() {
783 $timeout.flush(0);
784 },
785 triggerCallbacks: function() {
786 this.triggerCallbackEvents();
787 this.triggerCallbackPromise();
788 },
789 triggerReflow: function() {
784790 angular.forEach(reflowQueue, function(fn) {
785791 fn();
786792 });
789795 };
790796
791797 angular.forEach(
792 ['enter','leave','move','addClass','removeClass','setClass'], function(method) {
798 ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) {
793799 animate[method] = function() {
794800 animate.queue.push({
795 event : method,
796 element : arguments[0],
797 args : arguments
801 event: method,
802 element: arguments[0],
803 options: arguments[arguments.length - 1],
804 args: arguments
798805 });
799 $delegate[method].apply($delegate, arguments);
806 return $delegate[method].apply($delegate, arguments);
800807 };
801808 });
802809
803810 return animate;
804 });
811 }]);
805812
806813 }]);
807814
861868 function serializeScope(scope, offset) {
862869 offset = offset || ' ';
863870 var log = [offset + 'Scope(' + scope.$id + '): {'];
864 for ( var key in scope ) {
871 for (var key in scope) {
865872 if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
866873 log.push(' ' + key + ': ' + angular.toJson(scope[key]));
867874 }
868875 }
869876 var child = scope.$$childHead;
870 while(child) {
877 while (child) {
871878 log.push(serializeScope(child, offset + ' '));
872879 child = child.$$nextSibling;
873880 }
979986 * First we create the controller under test:
980987 *
981988 ```js
989 // The module code
990 angular
991 .module('MyApp', [])
992 .controller('MyController', MyController);
993
982994 // The controller code
983995 function MyController($scope, $http) {
984996 var authToken;
10061018 ```js
10071019 // testing controller
10081020 describe('MyController', function() {
1009 var $httpBackend, $rootScope, createController;
1021 var $httpBackend, $rootScope, createController, authRequestHandler;
1022
1023 // Set up the module
1024 beforeEach(module('MyApp'));
10101025
10111026 beforeEach(inject(function($injector) {
10121027 // Set up the mock http service responses
10131028 $httpBackend = $injector.get('$httpBackend');
10141029 // backend definition common for all tests
1015 $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
1030 authRequestHandler = $httpBackend.when('GET', '/auth.py')
1031 .respond({userId: 'userX'}, {'A-Token': 'xxx'});
10161032
10171033 // Get hold of a scope (i.e. the root scope)
10181034 $rootScope = $injector.get('$rootScope');
10351051 $httpBackend.expectGET('/auth.py');
10361052 var controller = createController();
10371053 $httpBackend.flush();
1054 });
1055
1056
1057 it('should fail authentication', function() {
1058
1059 // Notice how you can change the response even after it was set
1060 authRequestHandler.respond(401, '');
1061
1062 $httpBackend.expectGET('/auth.py');
1063 var controller = createController();
1064 $httpBackend.flush();
1065 expect($rootScope.status).toBe('Failed...');
10381066 });
10391067
10401068
10601088 $httpBackend.flush();
10611089
10621090 $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
1063 // check if the header was send, if it wasn't the expectation won't
1091 // check if the header was sent, if it wasn't the expectation won't
10641092 // match the request and the test will fail
10651093 return headers['Authorization'] == 'xxx';
10661094 }).respond(201, '');
10721100 ```
10731101 */
10741102 angular.mock.$HttpBackendProvider = function() {
1075 this.$get = ['$rootScope', createHttpBackendMock];
1103 this.$get = ['$rootScope', '$timeout', createHttpBackendMock];
10761104 };
10771105
10781106 /**
10891117 * @param {Object=} $browser Auto-flushing enabled if specified
10901118 * @return {Object} Instance of $httpBackend mock
10911119 */
1092 function createHttpBackendMock($rootScope, $delegate, $browser) {
1120 function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
10931121 var definitions = [],
10941122 expectations = [],
10951123 responses = [],
11021130 return function() {
11031131 return angular.isNumber(status)
11041132 ? [status, data, headers, statusText]
1105 : [200, status, data];
1133 : [200, status, data, headers];
11061134 };
11071135 }
11081136
11191147 }
11201148
11211149 function wrapResponse(wrapped) {
1122 if (!$browser && timeout && timeout.then) timeout.then(handleTimeout);
1150 if (!$browser && timeout) {
1151 timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout);
1152 }
11231153
11241154 return handleResponse;
11251155
11421172 }
11431173
11441174 if (expectation && expectation.match(method, url)) {
1145 if (!expectation.matchData(data))
1175 if (!expectation.matchData(data)) {
11461176 throw new Error('Expected ' + expectation + ' with different data\n' +
11471177 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
1148
1149 if (!expectation.matchHeaders(headers))
1178 }
1179
1180 if (!expectation.matchHeaders(headers)) {
11501181 throw new Error('Expected ' + expectation + ' with different headers\n' +
11511182 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
11521183 prettyPrint(headers));
1184 }
11531185
11541186 expectations.shift();
11551187
11851217 * Creates a new backend definition.
11861218 *
11871219 * @param {string} method HTTP method.
1188 * @param {string|RegExp} url HTTP url.
1220 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1221 * and returns true if the url matches the current definition.
11891222 * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
11901223 * data string and returns true if the data is as expected.
11911224 * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
11921225 * object and returns true if the headers match the current definition.
11931226 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1194 * request is handled.
1227 * request is handled. You can save this object for later use and invoke `respond` again in
1228 * order to change how a matched request is handled.
11951229 *
11961230 * - respond –
11971231 * `{function([status,] data[, headers, statusText])
11981232 * | function(function(method, url, data, headers)}`
11991233 * – The respond method takes a set of static data to be returned or a function that can
12001234 * return an array containing response status (number), response data (string), response
1201 * headers (Object), and the text for the status (string).
1235 * headers (Object), and the text for the status (string). The respond method returns the
1236 * `requestHandler` object for possible overrides.
12021237 */
12031238 $httpBackend.when = function(method, url, data, headers) {
12041239 var definition = new MockHttpExpectation(method, url, data, headers),
12051240 chain = {
12061241 respond: function(status, data, headers, statusText) {
1242 definition.passThrough = undefined;
12071243 definition.response = createResponse(status, data, headers, statusText);
1244 return chain;
12081245 }
12091246 };
12101247
12111248 if ($browser) {
12121249 chain.passThrough = function() {
1250 definition.response = undefined;
12131251 definition.passThrough = true;
1252 return chain;
12141253 };
12151254 }
12161255
12241263 * @description
12251264 * Creates a new backend definition for GET requests. For more info see `when()`.
12261265 *
1227 * @param {string|RegExp} url HTTP url.
1266 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1267 * and returns true if the url matches the current definition.
12281268 * @param {(Object|function(Object))=} headers HTTP headers.
1229 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1230 * request is handled.
1269 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1270 * request is handled. You can save this object for later use and invoke `respond` again in
1271 * order to change how a matched request is handled.
12311272 */
12321273
12331274 /**
12361277 * @description
12371278 * Creates a new backend definition for HEAD requests. For more info see `when()`.
12381279 *
1239 * @param {string|RegExp} url HTTP url.
1280 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1281 * and returns true if the url matches the current definition.
12401282 * @param {(Object|function(Object))=} headers HTTP headers.
1241 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1242 * request is handled.
1283 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1284 * request is handled. You can save this object for later use and invoke `respond` again in
1285 * order to change how a matched request is handled.
12431286 */
12441287
12451288 /**
12481291 * @description
12491292 * Creates a new backend definition for DELETE requests. For more info see `when()`.
12501293 *
1251 * @param {string|RegExp} url HTTP url.
1294 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1295 * and returns true if the url matches the current definition.
12521296 * @param {(Object|function(Object))=} headers HTTP headers.
1253 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1254 * request is handled.
1297 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1298 * request is handled. You can save this object for later use and invoke `respond` again in
1299 * order to change how a matched request is handled.
12551300 */
12561301
12571302 /**
12601305 * @description
12611306 * Creates a new backend definition for POST requests. For more info see `when()`.
12621307 *
1263 * @param {string|RegExp} url HTTP url.
1308 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1309 * and returns true if the url matches the current definition.
12641310 * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
12651311 * data string and returns true if the data is as expected.
12661312 * @param {(Object|function(Object))=} headers HTTP headers.
1267 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1268 * request is handled.
1313 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1314 * request is handled. You can save this object for later use and invoke `respond` again in
1315 * order to change how a matched request is handled.
12691316 */
12701317
12711318 /**
12741321 * @description
12751322 * Creates a new backend definition for PUT requests. For more info see `when()`.
12761323 *
1277 * @param {string|RegExp} url HTTP url.
1324 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1325 * and returns true if the url matches the current definition.
12781326 * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
12791327 * data string and returns true if the data is as expected.
12801328 * @param {(Object|function(Object))=} headers HTTP headers.
1281 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1282 * request is handled.
1329 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1330 * request is handled. You can save this object for later use and invoke `respond` again in
1331 * order to change how a matched request is handled.
12831332 */
12841333
12851334 /**
12881337 * @description
12891338 * Creates a new backend definition for JSONP requests. For more info see `when()`.
12901339 *
1291 * @param {string|RegExp} url HTTP url.
1292 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1293 * request is handled.
1340 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1341 * and returns true if the url matches the current definition.
1342 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1343 * request is handled. You can save this object for later use and invoke `respond` again in
1344 * order to change how a matched request is handled.
12941345 */
12951346 createShortMethods('when');
12961347
13021353 * Creates a new request expectation.
13031354 *
13041355 * @param {string} method HTTP method.
1305 * @param {string|RegExp} url HTTP url.
1356 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1357 * and returns true if the url matches the current definition.
13061358 * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
13071359 * receives data string and returns true if the data is as expected, or Object if request body
13081360 * is in JSON format.
13091361 * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
13101362 * object and returns true if the headers match the current expectation.
1311 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1312 * request is handled.
1363 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1364 * request is handled. You can save this object for later use and invoke `respond` again in
1365 * order to change how a matched request is handled.
13131366 *
13141367 * - respond –
13151368 * `{function([status,] data[, headers, statusText])
13161369 * | function(function(method, url, data, headers)}`
13171370 * – The respond method takes a set of static data to be returned or a function that can
13181371 * return an array containing response status (number), response data (string), response
1319 * headers (Object), and the text for the status (string).
1372 * headers (Object), and the text for the status (string). The respond method returns the
1373 * `requestHandler` object for possible overrides.
13201374 */
13211375 $httpBackend.expect = function(method, url, data, headers) {
1322 var expectation = new MockHttpExpectation(method, url, data, headers);
1376 var expectation = new MockHttpExpectation(method, url, data, headers),
1377 chain = {
1378 respond: function(status, data, headers, statusText) {
1379 expectation.response = createResponse(status, data, headers, statusText);
1380 return chain;
1381 }
1382 };
1383
13231384 expectations.push(expectation);
1324 return {
1325 respond: function (status, data, headers, statusText) {
1326 expectation.response = createResponse(status, data, headers, statusText);
1327 }
1328 };
1385 return chain;
13291386 };
13301387
13311388
13351392 * @description
13361393 * Creates a new request expectation for GET requests. For more info see `expect()`.
13371394 *
1338 * @param {string|RegExp} url HTTP url.
1395 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1396 * and returns true if the url matches the current definition.
13391397 * @param {Object=} headers HTTP headers.
1340 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1341 * request is handled. See #expect for more info.
1398 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1399 * request is handled. You can save this object for later use and invoke `respond` again in
1400 * order to change how a matched request is handled. See #expect for more info.
13421401 */
13431402
13441403 /**
13471406 * @description
13481407 * Creates a new request expectation for HEAD requests. For more info see `expect()`.
13491408 *
1350 * @param {string|RegExp} url HTTP url.
1409 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1410 * and returns true if the url matches the current definition.
13511411 * @param {Object=} headers HTTP headers.
1352 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1353 * request is handled.
1412 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1413 * request is handled. You can save this object for later use and invoke `respond` again in
1414 * order to change how a matched request is handled.
13541415 */
13551416
13561417 /**
13591420 * @description
13601421 * Creates a new request expectation for DELETE requests. For more info see `expect()`.
13611422 *
1362 * @param {string|RegExp} url HTTP url.
1423 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1424 * and returns true if the url matches the current definition.
13631425 * @param {Object=} headers HTTP headers.
1364 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1365 * request is handled.
1426 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1427 * request is handled. You can save this object for later use and invoke `respond` again in
1428 * order to change how a matched request is handled.
13661429 */
13671430
13681431 /**
13711434 * @description
13721435 * Creates a new request expectation for POST requests. For more info see `expect()`.
13731436 *
1374 * @param {string|RegExp} url HTTP url.
1437 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1438 * and returns true if the url matches the current definition.
13751439 * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
13761440 * receives data string and returns true if the data is as expected, or Object if request body
13771441 * is in JSON format.
13781442 * @param {Object=} headers HTTP headers.
1379 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1380 * request is handled.
1443 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1444 * request is handled. You can save this object for later use and invoke `respond` again in
1445 * order to change how a matched request is handled.
13811446 */
13821447
13831448 /**
13861451 * @description
13871452 * Creates a new request expectation for PUT requests. For more info see `expect()`.
13881453 *
1389 * @param {string|RegExp} url HTTP url.
1454 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1455 * and returns true if the url matches the current definition.
13901456 * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
13911457 * receives data string and returns true if the data is as expected, or Object if request body
13921458 * is in JSON format.
13931459 * @param {Object=} headers HTTP headers.
1394 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1395 * request is handled.
1460 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1461 * request is handled. You can save this object for later use and invoke `respond` again in
1462 * order to change how a matched request is handled.
13961463 */
13971464
13981465 /**
14011468 * @description
14021469 * Creates a new request expectation for PATCH requests. For more info see `expect()`.
14031470 *
1404 * @param {string|RegExp} url HTTP url.
1471 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1472 * and returns true if the url matches the current definition.
14051473 * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
14061474 * receives data string and returns true if the data is as expected, or Object if request body
14071475 * is in JSON format.
14081476 * @param {Object=} headers HTTP headers.
1409 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1410 * request is handled.
1477 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1478 * request is handled. You can save this object for later use and invoke `respond` again in
1479 * order to change how a matched request is handled.
14111480 */
14121481
14131482 /**
14161485 * @description
14171486 * Creates a new request expectation for JSONP requests. For more info see `expect()`.
14181487 *
1419 * @param {string|RegExp} url HTTP url.
1420 * @returns {requestHandler} Returns an object with `respond` method that control how a matched
1421 * request is handled.
1488 * @param {string|RegExp|function(string)} url HTTP url or function that receives an url
1489 * and returns true if the url matches the current definition.
1490 * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
1491 * request is handled. You can save this object for later use and invoke `respond` again in
1492 * order to change how a matched request is handled.
14221493 */
14231494 createShortMethods('expect');
14241495
14331504 * all pending requests will be flushed. If there are no pending requests when the flush method
14341505 * is called an exception is thrown (as this typically a sign of programming error).
14351506 */
1436 $httpBackend.flush = function(count) {
1437 $rootScope.$digest();
1507 $httpBackend.flush = function(count, digest) {
1508 if (digest !== false) $rootScope.$digest();
14381509 if (!responses.length) throw new Error('No pending request to flush !');
14391510
1440 if (angular.isDefined(count)) {
1511 if (angular.isDefined(count) && count !== null) {
14411512 while (count--) {
14421513 if (!responses.length) throw new Error('No more pending request to flush !');
14431514 responses.shift()();
14471518 responses.shift()();
14481519 }
14491520 }
1450 $httpBackend.verifyNoOutstandingExpectation();
1521 $httpBackend.verifyNoOutstandingExpectation(digest);
14511522 };
14521523
14531524
14651536 * afterEach($httpBackend.verifyNoOutstandingExpectation);
14661537 * ```
14671538 */
1468 $httpBackend.verifyNoOutstandingExpectation = function() {
1469 $rootScope.$digest();
1539 $httpBackend.verifyNoOutstandingExpectation = function(digest) {
1540 if (digest !== false) $rootScope.$digest();
14701541 if (expectations.length) {
14711542 throw new Error('Unsatisfied requests: ' + expectations.join(', '));
14721543 }
15101581
15111582
15121583 function createShortMethods(prefix) {
1513 angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
1584 angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) {
15141585 $httpBackend[prefix + method] = function(url, headers) {
15151586 return $httpBackend[prefix](method, url, undefined, headers);
15161587 };
15401611 this.matchUrl = function(u) {
15411612 if (!url) return true;
15421613 if (angular.isFunction(url.test)) return url.test(u);
1614 if (angular.isFunction(url)) return url(u);
15431615 return url == u;
15441616 };
15451617
15531625 if (angular.isUndefined(data)) return true;
15541626 if (data && angular.isFunction(data.test)) return data.test(d);
15551627 if (data && angular.isFunction(data)) return data(d);
1556 if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d));
1628 if (data && !angular.isString(data)) {
1629 return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d));
1630 }
15571631 return data == d;
15581632 };
15591633
16261700 * that adds a "flush" and "verifyNoPendingTasks" methods.
16271701 */
16281702
1629 angular.mock.$TimeoutDecorator = function($delegate, $browser) {
1703 angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) {
16301704
16311705 /**
16321706 * @ngdoc method
16651739 }
16661740
16671741 return $delegate;
1668 };
1669
1670 angular.mock.$RAFDecorator = function($delegate) {
1742 }];
1743
1744 angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
16711745 var queue = [];
16721746 var rafFn = function(fn) {
16731747 var index = queue.length;
16801754 rafFn.supported = $delegate.supported;
16811755
16821756 rafFn.flush = function() {
1683 if(queue.length === 0) {
1757 if (queue.length === 0) {
16841758 throw new Error('No rAF callbacks present');
16851759 }
16861760
16871761 var length = queue.length;
1688 for(var i=0;i<length;i++) {
1762 for (var i = 0; i < length; i++) {
16891763 queue[i]();
16901764 }
16911765
1692 queue = [];
1766 queue = queue.slice(i);
16931767 };
16941768
16951769 return rafFn;
1696 };
1697
1698 angular.mock.$AsyncCallbackDecorator = function($delegate) {
1699 var callbacks = [];
1700 var addFn = function(fn) {
1701 callbacks.push(fn);
1702 };
1703 addFn.flush = function() {
1704 angular.forEach(callbacks, function(fn) {
1705 fn();
1706 });
1707 callbacks = [];
1708 };
1709 return addFn;
1710 };
1770 }];
17111771
17121772 /**
17131773 *
17171777 return angular.element('<div ng-app></div>');
17181778 };
17191779 };
1780
1781 /**
1782 * @ngdoc service
1783 * @name $controller
1784 * @description
1785 * A decorator for {@link ng.$controller} with additional `bindings` parameter, useful when testing
1786 * controllers of directives that use {@link $compile#-bindtocontroller- `bindToController`}.
1787 *
1788 *
1789 * ## Example
1790 *
1791 * ```js
1792 *
1793 * // Directive definition ...
1794 *
1795 * myMod.directive('myDirective', {
1796 * controller: 'MyDirectiveController',
1797 * bindToController: {
1798 * name: '@'
1799 * }
1800 * });
1801 *
1802 *
1803 * // Controller definition ...
1804 *
1805 * myMod.controller('MyDirectiveController', ['log', function($log) {
1806 * $log.info(this.name);
1807 * })];
1808 *
1809 *
1810 * // In a test ...
1811 *
1812 * describe('myDirectiveController', function() {
1813 * it('should write the bound name to the log', inject(function($controller, $log) {
1814 * var ctrl = $controller('MyDirective', { /* no locals &#42;/ }, { name: 'Clark Kent' });
1815 * expect(ctrl.name).toEqual('Clark Kent');
1816 * expect($log.info.logs).toEqual(['Clark Kent']);
1817 * });
1818 * });
1819 *
1820 * ```
1821 *
1822 * @param {Function|string} constructor If called with a function then it's considered to be the
1823 * controller constructor function. Otherwise it's considered to be a string which is used
1824 * to retrieve the controller constructor using the following steps:
1825 *
1826 * * check if a controller with given name is registered via `$controllerProvider`
1827 * * check if evaluating the string on the current scope returns a constructor
1828 * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
1829 * `window` object (not recommended)
1830 *
1831 * The string can use the `controller as property` syntax, where the controller instance is published
1832 * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
1833 * to work correctly.
1834 *
1835 * @param {Object} locals Injection locals for Controller.
1836 * @param {Object=} bindings Properties to add to the controller before invoking the constructor. This is used
1837 * to simulate the `bindToController` feature and simplify certain kinds of tests.
1838 * @return {Object} Instance of given controller.
1839 */
1840 angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
1841 return function(expression, locals, later, ident) {
1842 if (later && typeof later === 'object') {
1843 var create = $delegate(expression, locals, true, ident);
1844 angular.extend(create.instance, later);
1845 return create();
1846 }
1847 return $delegate(expression, locals, later, ident);
1848 };
1849 }];
1850
17201851
17211852 /**
17221853 * @ngdoc module
17441875 }).config(['$provide', function($provide) {
17451876 $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
17461877 $provide.decorator('$$rAF', angular.mock.$RAFDecorator);
1747 $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
1878 $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
1879 $provide.decorator('$controller', angular.mock.$ControllerDecorator);
17481880 }]);
17491881
17501882 /**
18211953 * Creates a new backend definition.
18221954 *
18231955 * @param {string} method HTTP method.
1824 * @param {string|RegExp} url HTTP url.
1956 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1957 * and returns true if the url matches the current definition.
18251958 * @param {(string|RegExp)=} data HTTP request body.
18261959 * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
18271960 * object and returns true if the headers match the current definition.
18281961 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1829 * control how a matched request is handled.
1962 * control how a matched request is handled. You can save this object for later use and invoke
1963 * `respond` or `passThrough` again in order to change how a matched request is handled.
18301964 *
18311965 * - respond –
18321966 * `{function([status,] data[, headers, statusText])
18371971 * - passThrough – `{function()}` – Any request matching a backend definition with
18381972 * `passThrough` handler will be passed through to the real backend (an XHR request will be made
18391973 * to the server.)
1974 * - Both methods return the `requestHandler` object for possible overrides.
18401975 */
18411976
18421977 /**
18461981 * @description
18471982 * Creates a new backend definition for GET requests. For more info see `when()`.
18481983 *
1849 * @param {string|RegExp} url HTTP url.
1984 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
1985 * and returns true if the url matches the current definition.
18501986 * @param {(Object|function(Object))=} headers HTTP headers.
18511987 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1852 * control how a matched request is handled.
1988 * control how a matched request is handled. You can save this object for later use and invoke
1989 * `respond` or `passThrough` again in order to change how a matched request is handled.
18531990 */
18541991
18551992 /**
18591996 * @description
18601997 * Creates a new backend definition for HEAD requests. For more info see `when()`.
18611998 *
1862 * @param {string|RegExp} url HTTP url.
1999 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
2000 * and returns true if the url matches the current definition.
18632001 * @param {(Object|function(Object))=} headers HTTP headers.
18642002 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1865 * control how a matched request is handled.
2003 * control how a matched request is handled. You can save this object for later use and invoke
2004 * `respond` or `passThrough` again in order to change how a matched request is handled.
18662005 */
18672006
18682007 /**
18722011 * @description
18732012 * Creates a new backend definition for DELETE requests. For more info see `when()`.
18742013 *
1875 * @param {string|RegExp} url HTTP url.
2014 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
2015 * and returns true if the url matches the current definition.
18762016 * @param {(Object|function(Object))=} headers HTTP headers.
18772017 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1878 * control how a matched request is handled.
2018 * control how a matched request is handled. You can save this object for later use and invoke
2019 * `respond` or `passThrough` again in order to change how a matched request is handled.
18792020 */
18802021
18812022 /**
18852026 * @description
18862027 * Creates a new backend definition for POST requests. For more info see `when()`.
18872028 *
1888 * @param {string|RegExp} url HTTP url.
2029 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
2030 * and returns true if the url matches the current definition.
18892031 * @param {(string|RegExp)=} data HTTP request body.
18902032 * @param {(Object|function(Object))=} headers HTTP headers.
18912033 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1892 * control how a matched request is handled.
2034 * control how a matched request is handled. You can save this object for later use and invoke
2035 * `respond` or `passThrough` again in order to change how a matched request is handled.
18932036 */
18942037
18952038 /**
18992042 * @description
19002043 * Creates a new backend definition for PUT requests. For more info see `when()`.
19012044 *
1902 * @param {string|RegExp} url HTTP url.
2045 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
2046 * and returns true if the url matches the current definition.
19032047 * @param {(string|RegExp)=} data HTTP request body.
19042048 * @param {(Object|function(Object))=} headers HTTP headers.
19052049 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1906 * control how a matched request is handled.
2050 * control how a matched request is handled. You can save this object for later use and invoke
2051 * `respond` or `passThrough` again in order to change how a matched request is handled.
19072052 */
19082053
19092054 /**
19132058 * @description
19142059 * Creates a new backend definition for PATCH requests. For more info see `when()`.
19152060 *
1916 * @param {string|RegExp} url HTTP url.
2061 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
2062 * and returns true if the url matches the current definition.
19172063 * @param {(string|RegExp)=} data HTTP request body.
19182064 * @param {(Object|function(Object))=} headers HTTP headers.
19192065 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1920 * control how a matched request is handled.
2066 * control how a matched request is handled. You can save this object for later use and invoke
2067 * `respond` or `passThrough` again in order to change how a matched request is handled.
19212068 */
19222069
19232070 /**
19272074 * @description
19282075 * Creates a new backend definition for JSONP requests. For more info see `when()`.
19292076 *
1930 * @param {string|RegExp} url HTTP url.
2077 * @param {string|RegExp|function(string)} url HTTP url or function that receives a url
2078 * and returns true if the url matches the current definition.
19312079 * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
1932 * control how a matched request is handled.
2080 * control how a matched request is handled. You can save this object for later use and invoke
2081 * `respond` or `passThrough` again in order to change how a matched request is handled.
19332082 */
19342083 angular.mock.e2e = {};
19352084 angular.mock.e2e.$httpBackendDecorator =
1936 ['$rootScope', '$delegate', '$browser', createHttpBackendMock];
1937
1938
1939 angular.mock.clearDataCache = function() {
1940 var key,
1941 cache = angular.element.cache;
1942
1943 for(key in cache) {
1944 if (Object.prototype.hasOwnProperty.call(cache,key)) {
1945 var handle = cache[key].handle;
1946
1947 handle && angular.element(handle.elem).off();
1948 delete cache[key];
1949 }
2085 ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock];
2086
2087
2088 /**
2089 * @ngdoc type
2090 * @name $rootScope.Scope
2091 * @module ngMock
2092 * @description
2093 * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These
2094 * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when
2095 * `ngMock` module is loaded.
2096 *
2097 * In addition to all the regular `Scope` methods, the following helper methods are available:
2098 */
2099 angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
2100
2101 var $rootScopePrototype = Object.getPrototypeOf($delegate);
2102
2103 $rootScopePrototype.$countChildScopes = countChildScopes;
2104 $rootScopePrototype.$countWatchers = countWatchers;
2105
2106 return $delegate;
2107
2108 // ------------------------------------------------------------------------------------------ //
2109
2110 /**
2111 * @ngdoc method
2112 * @name $rootScope.Scope#$countChildScopes
2113 * @module ngMock
2114 * @description
2115 * Counts all the direct and indirect child scopes of the current scope.
2116 *
2117 * The current scope is excluded from the count. The count includes all isolate child scopes.
2118 *
2119 * @returns {number} Total number of child scopes.
2120 */
2121 function countChildScopes() {
2122 // jshint validthis: true
2123 var count = 0; // exclude the current scope
2124 var pendingChildHeads = [this.$$childHead];
2125 var currentScope;
2126
2127 while (pendingChildHeads.length) {
2128 currentScope = pendingChildHeads.shift();
2129
2130 while (currentScope) {
2131 count += 1;
2132 pendingChildHeads.push(currentScope.$$childHead);
2133 currentScope = currentScope.$$nextSibling;
2134 }
2135 }
2136
2137 return count;
19502138 }
1951 };
1952
1953
1954 if(window.jasmine || window.mocha) {
2139
2140
2141 /**
2142 * @ngdoc method
2143 * @name $rootScope.Scope#$countWatchers
2144 * @module ngMock
2145 * @description
2146 * Counts all the watchers of direct and indirect child scopes of the current scope.
2147 *
2148 * The watchers of the current scope are included in the count and so are all the watchers of
2149 * isolate child scopes.
2150 *
2151 * @returns {number} Total number of watchers.
2152 */
2153 function countWatchers() {
2154 // jshint validthis: true
2155 var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope
2156 var pendingChildHeads = [this.$$childHead];
2157 var currentScope;
2158
2159 while (pendingChildHeads.length) {
2160 currentScope = pendingChildHeads.shift();
2161
2162 while (currentScope) {
2163 count += currentScope.$$watchers ? currentScope.$$watchers.length : 0;
2164 pendingChildHeads.push(currentScope.$$childHead);
2165 currentScope = currentScope.$$nextSibling;
2166 }
2167 }
2168
2169 return count;
2170 }
2171 }];
2172
2173
2174 if (window.jasmine || window.mocha) {
19552175
19562176 var currentSpec = null,
2177 annotatedFunctions = [],
19572178 isSpecRunning = function() {
19582179 return !!currentSpec;
19592180 };
19602181
2182 angular.mock.$$annotate = angular.injector.$$annotate;
2183 angular.injector.$$annotate = function(fn) {
2184 if (typeof fn === 'function' && !fn.$inject) {
2185 annotatedFunctions.push(fn);
2186 }
2187 return angular.mock.$$annotate.apply(this, arguments);
2188 };
2189
19612190
19622191 (window.beforeEach || window.setup)(function() {
2192 annotatedFunctions = [];
19632193 currentSpec = this;
19642194 });
19652195
19662196 (window.afterEach || window.teardown)(function() {
19672197 var injector = currentSpec.$injector;
2198
2199 annotatedFunctions.forEach(function(fn) {
2200 delete fn.$inject;
2201 });
19682202
19692203 angular.forEach(currentSpec.$modules, function(module) {
19702204 if (module && module.$$hashKey) {
19782212
19792213 if (injector) {
19802214 injector.get('$rootElement').off();
1981 injector.get('$browser').pollFns.length = 0;
1982 }
1983
1984 angular.mock.clearDataCache();
2215 }
19852216
19862217 // clean up jquery's fragment cache
19872218 angular.forEach(angular.element.fragments, function(val, key) {
21452376 /////////////////////
21462377 function workFn() {
21472378 var modules = currentSpec.$modules || [];
2148
2379 var strictDi = !!currentSpec.$injectorStrict;
21492380 modules.unshift('ngMock');
21502381 modules.unshift('ng');
21512382 var injector = currentSpec.$injector;
21522383 if (!injector) {
2153 injector = currentSpec.$injector = angular.injector(modules);
2384 if (strictDi) {
2385 // If strictDi is enabled, annotate the providerInjector blocks
2386 angular.forEach(modules, function(moduleFn) {
2387 if (typeof moduleFn === "function") {
2388 angular.injector.$$annotate(moduleFn);
2389 }
2390 });
2391 }
2392 injector = currentSpec.$injector = angular.injector(modules, strictDi);
2393 currentSpec.$injectorStrict = strictDi;
21542394 }
2155 for(var i = 0, ii = blockFns.length; i < ii; i++) {
2395 for (var i = 0, ii = blockFns.length; i < ii; i++) {
2396 if (currentSpec.$injectorStrict) {
2397 // If the injector is strict / strictDi, and the spec wants to inject using automatic
2398 // annotation, then annotate the function here.
2399 injector.annotate(blockFns[i]);
2400 }
21562401 try {
21572402 /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
21582403 injector.invoke(blockFns[i] || angular.noop, this);
21682413 }
21692414 }
21702415 };
2416
2417
2418 angular.mock.inject.strictDi = function(value) {
2419 value = arguments.length ? !!value : true;
2420 return isSpecRunning() ? workFn() : workFn;
2421
2422 function workFn() {
2423 if (value !== currentSpec.$injectorStrict) {
2424 if (currentSpec.$injector) {
2425 throw new Error('Injector already created, can not modify strict annotations');
2426 } else {
2427 currentSpec.$injectorStrict = value;
2428 }
2429 }
2430 }
2431 };
21712432 }
21722433
21732434
00 /**
1 * @license AngularJS v1.2.23
2 * (c) 2010-2014 Google, Inc. http://angularjs.org
1 * @license AngularJS v1.4.3
2 * (c) 2010-2015 Google, Inc. http://angularjs.org
33 * License: MIT
44 */
55 (function(window, angular, undefined) {'use strict';
2121 */
2222 /* global -ngRouteModule */
2323 var ngRouteModule = angular.module('ngRoute', ['ng']).
24 provider('$route', $RouteProvider);
24 provider('$route', $RouteProvider),
25 $routeMinErr = angular.$$minErr('ngRoute');
2526
2627 /**
2728 * @ngdoc provider
2829 * @name $routeProvider
29 * @kind function
3030 *
3131 * @description
3232 *
3838 * ## Dependencies
3939 * Requires the {@link ngRoute `ngRoute`} module to be installed.
4040 */
41 function $RouteProvider(){
41 function $RouteProvider() {
4242 function inherit(parent, extra) {
43 return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra);
43 return angular.extend(Object.create(parent), extra);
4444 }
4545
4646 var routes = {};
7777 * - `controller` – `{(string|function()=}` – Controller fn that should be associated with
7878 * newly created scope or the name of a {@link angular.Module#controller registered
7979 * controller} if passed as a string.
80 * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be
81 * published to scope under the `controllerAs` name.
80 * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller.
81 * If present, the controller will be published to scope under the `controllerAs` name.
8282 * - `template` – `{string=|function()=}` – html template as a string or a function that
8383 * returns an html template as a string which should be used by {@link
8484 * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
145145 * Adds a new route definition to the `$route` service.
146146 */
147147 this.when = function(path, route) {
148 //copy original route object to preserve params inherited from proto chain
149 var routeCopy = angular.copy(route);
150 if (angular.isUndefined(routeCopy.reloadOnSearch)) {
151 routeCopy.reloadOnSearch = true;
152 }
153 if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) {
154 routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch;
155 }
148156 routes[path] = angular.extend(
149 {reloadOnSearch: true},
150 route,
151 path && pathRegExp(path, route)
157 routeCopy,
158 path && pathRegExp(path, routeCopy)
152159 );
153160
154161 // create redirection for trailing slashes
155162 if (path) {
156 var redirectPath = (path[path.length-1] == '/')
157 ? path.substr(0, path.length-1)
158 : path +'/';
163 var redirectPath = (path[path.length - 1] == '/')
164 ? path.substr(0, path.length - 1)
165 : path + '/';
159166
160167 routes[redirectPath] = angular.extend(
161168 {redirectTo: path},
162 pathRegExp(redirectPath, route)
169 pathRegExp(redirectPath, routeCopy)
163170 );
164171 }
165172
166173 return this;
167174 };
175
176 /**
177 * @ngdoc property
178 * @name $routeProvider#caseInsensitiveMatch
179 * @description
180 *
181 * A boolean property indicating if routes defined
182 * using this provider should be matched using a case insensitive
183 * algorithm. Defaults to `false`.
184 */
185 this.caseInsensitiveMatch = false;
168186
169187 /**
170188 * @param path {string} path
187205
188206 path = path
189207 .replace(/([().])/g, '\\$1')
190 .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){
208 .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) {
191209 var optional = option === '?' ? option : null;
192210 var star = option === '*' ? option : null;
193211 keys.push({ name: key, optional: !!optional });
215233 * Sets route definition that will be used on route change when no other route definition
216234 * is matched.
217235 *
218 * @param {Object} params Mapping information to be assigned to `$route.current`.
236 * @param {Object|string} params Mapping information to be assigned to `$route.current`.
237 * If called with a string, the value maps to `redirectTo`.
219238 * @returns {Object} self
220239 */
221240 this.otherwise = function(params) {
241 if (typeof params === 'string') {
242 params = {redirectTo: params};
243 }
222244 this.when(null, params);
223245 return this;
224246 };
229251 '$routeParams',
230252 '$q',
231253 '$injector',
232 '$http',
233 '$templateCache',
254 '$templateRequest',
234255 '$sce',
235 function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) {
256 function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) {
236257
237258 /**
238259 * @ngdoc service
268289 * @example
269290 * This example shows how changing the URL hash causes the `$route` to match a route against the
270291 * URL, and the `ngView` pulls in the partial.
271 *
272 * Note that this example is using {@link ng.directive:script inlined templates}
273 * to get it working on jsfiddle as well.
274292 *
275293 * <example name="$route-service" module="ngRouteExample"
276294 * deps="angular-route.js" fixBase="true">
379397 * defined in `resolve` route property. Once all of the dependencies are resolved
380398 * `$routeChangeSuccess` is fired.
381399 *
400 * The route change (and the `$location` change that triggered it) can be prevented
401 * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on}
402 * for more details about event object.
403 *
382404 * @param {Object} angularEvent Synthetic event object.
383405 * @param {Route} next Future route information.
384406 * @param {Route} current Current route information.
389411 * @name $route#$routeChangeSuccess
390412 * @eventType broadcast on root scope
391413 * @description
392 * Broadcasted after a route dependencies are resolved.
414 * Broadcasted after a route change has happened successfully.
415 * The `resolve` dependencies are now available in the `current.locals` property.
416 *
393417 * {@link ngRoute.directive:ngView ngView} listens for the directive
394418 * to instantiate the controller and render the view.
395419 *
417441 * @name $route#$routeUpdate
418442 * @eventType broadcast on root scope
419443 * @description
420 *
421444 * The `reloadOnSearch` property has been set to false, and we are reusing the same
422445 * instance of the Controller.
446 *
447 * @param {Object} angularEvent Synthetic event object
448 * @param {Route} current Current/previous route information.
423449 */
424450
425451 var forceReload = false,
452 preparedRoute,
453 preparedRouteIsUpdateOnly,
426454 $route = {
427455 routes: routes,
428456
435463 * {@link ng.$location $location} hasn't changed.
436464 *
437465 * As a result of that, {@link ngRoute.directive:ngView ngView}
438 * creates new scope, reinstantiates the controller.
466 * creates new scope and reinstantiates the controller.
439467 */
440468 reload: function() {
441469 forceReload = true;
442 $rootScope.$evalAsync(updateRoute);
470 $rootScope.$evalAsync(function() {
471 // Don't support cancellation of a reload for now...
472 prepareRoute();
473 commitRoute();
474 });
475 },
476
477 /**
478 * @ngdoc method
479 * @name $route#updateParams
480 *
481 * @description
482 * Causes `$route` service to update the current URL, replacing
483 * current route parameters with those specified in `newParams`.
484 * Provided property names that match the route's path segment
485 * definitions will be interpolated into the location's path, while
486 * remaining properties will be treated as query params.
487 *
488 * @param {!Object<string, string>} newParams mapping of URL parameter names to values
489 */
490 updateParams: function(newParams) {
491 if (this.current && this.current.$$route) {
492 newParams = angular.extend({}, this.current.params, newParams);
493 $location.path(interpolate(this.current.$$route.originalPath, newParams));
494 // interpolate modifies newParams, only query params are left
495 $location.search(newParams);
496 } else {
497 throw $routeMinErr('norout', 'Tried updating route when with no current route');
498 }
443499 }
444500 };
445501
446 $rootScope.$on('$locationChangeSuccess', updateRoute);
502 $rootScope.$on('$locationChangeStart', prepareRoute);
503 $rootScope.$on('$locationChangeSuccess', commitRoute);
447504
448505 return $route;
449506
481538 return params;
482539 }
483540
484 function updateRoute() {
485 var next = parseRoute(),
486 last = $route.current;
487
488 if (next && last && next.$$route === last.$$route
489 && angular.equals(next.pathParams, last.pathParams)
490 && !next.reloadOnSearch && !forceReload) {
491 last.params = next.params;
492 angular.copy(last.params, $routeParams);
493 $rootScope.$broadcast('$routeUpdate', last);
494 } else if (next || last) {
541 function prepareRoute($locationEvent) {
542 var lastRoute = $route.current;
543
544 preparedRoute = parseRoute();
545 preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
546 && angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
547 && !preparedRoute.reloadOnSearch && !forceReload;
548
549 if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
550 if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
551 if ($locationEvent) {
552 $locationEvent.preventDefault();
553 }
554 }
555 }
556 }
557
558 function commitRoute() {
559 var lastRoute = $route.current;
560 var nextRoute = preparedRoute;
561
562 if (preparedRouteIsUpdateOnly) {
563 lastRoute.params = nextRoute.params;
564 angular.copy(lastRoute.params, $routeParams);
565 $rootScope.$broadcast('$routeUpdate', lastRoute);
566 } else if (nextRoute || lastRoute) {
495567 forceReload = false;
496 $rootScope.$broadcast('$routeChangeStart', next, last);
497 $route.current = next;
498 if (next) {
499 if (next.redirectTo) {
500 if (angular.isString(next.redirectTo)) {
501 $location.path(interpolate(next.redirectTo, next.params)).search(next.params)
568 $route.current = nextRoute;
569 if (nextRoute) {
570 if (nextRoute.redirectTo) {
571 if (angular.isString(nextRoute.redirectTo)) {
572 $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params)
502573 .replace();
503574 } else {
504 $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
575 $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search()))
505576 .replace();
506577 }
507578 }
508579 }
509580
510 $q.when(next).
581 $q.when(nextRoute).
511582 then(function() {
512 if (next) {
513 var locals = angular.extend({}, next.resolve),
583 if (nextRoute) {
584 var locals = angular.extend({}, nextRoute.resolve),
514585 template, templateUrl;
515586
516587 angular.forEach(locals, function(value, key) {
517588 locals[key] = angular.isString(value) ?
518 $injector.get(value) : $injector.invoke(value);
589 $injector.get(value) : $injector.invoke(value, null, null, key);
519590 });
520591
521 if (angular.isDefined(template = next.template)) {
592 if (angular.isDefined(template = nextRoute.template)) {
522593 if (angular.isFunction(template)) {
523 template = template(next.params);
594 template = template(nextRoute.params);
524595 }
525 } else if (angular.isDefined(templateUrl = next.templateUrl)) {
596 } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) {
526597 if (angular.isFunction(templateUrl)) {
527 templateUrl = templateUrl(next.params);
598 templateUrl = templateUrl(nextRoute.params);
528599 }
529 templateUrl = $sce.getTrustedResourceUrl(templateUrl);
530600 if (angular.isDefined(templateUrl)) {
531 next.loadedTemplateUrl = templateUrl;
532 template = $http.get(templateUrl, {cache: $templateCache}).
533 then(function(response) { return response.data; });
601 nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl);
602 template = $templateRequest(templateUrl);
534603 }
535604 }
536605 if (angular.isDefined(template)) {
539608 return $q.all(locals);
540609 }
541610 }).
542 // after route change
543611 then(function(locals) {
544 if (next == $route.current) {
545 if (next) {
546 next.locals = locals;
547 angular.copy(next.params, $routeParams);
612 // after route change
613 if (nextRoute == $route.current) {
614 if (nextRoute) {
615 nextRoute.locals = locals;
616 angular.copy(nextRoute.params, $routeParams);
548617 }
549 $rootScope.$broadcast('$routeChangeSuccess', next, last);
618 $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute);
550619 }
551620 }, function(error) {
552 if (next == $route.current) {
553 $rootScope.$broadcast('$routeChangeError', next, last, error);
621 if (nextRoute == $route.current) {
622 $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error);
554623 }
555624 });
556625 }
580649 */
581650 function interpolate(string, params) {
582651 var result = [];
583 angular.forEach((string||'').split(':'), function(segment, i) {
652 angular.forEach((string || '').split(':'), function(segment, i) {
584653 if (i === 0) {
585654 result.push(segment);
586655 } else {
587 var segmentMatch = segment.match(/(\w+)(.*)/);
656 var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/);
588657 var key = segmentMatch[1];
589658 result.push(params[key]);
590659 result.push(segmentMatch[2] || '');
692761 <pre>$location.path() = {{main.$location.path()}}</pre>
693762 <pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
694763 <pre>$route.current.params = {{main.$route.current.params}}</pre>
695 <pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre>
696764 <pre>$routeParams = {{main.$routeParams}}</pre>
697765 </div>
698766 </file>
716784 .view-animate-container {
717785 position:relative;
718786 height:100px!important;
719 position:relative;
720787 background:white;
721788 border:1px solid black;
722789 height:40px;
770837 controllerAs: 'chapter'
771838 });
772839
773 // configure html5 to get links working on jsfiddle
774840 $locationProvider.html5Mode(true);
775841 }])
776842 .controller('MainCtrl', ['$route', '$routeParams', '$location',
817883 * Emitted every time the ngView content is reloaded.
818884 */
819885 ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate'];
820 function ngViewFactory( $route, $anchorScroll, $animate) {
886 function ngViewFactory($route, $anchorScroll, $animate) {
821887 return {
822888 restrict: 'ECA',
823889 terminal: true,
826892 link: function(scope, $element, attr, ctrl, $transclude) {
827893 var currentScope,
828894 currentElement,
829 previousElement,
895 previousLeaveAnimation,
830896 autoScrollExp = attr.autoscroll,
831897 onloadExp = attr.onload || '';
832898
834900 update();
835901
836902 function cleanupLastView() {
837 if(previousElement) {
838 previousElement.remove();
839 previousElement = null;
903 if (previousLeaveAnimation) {
904 $animate.cancel(previousLeaveAnimation);
905 previousLeaveAnimation = null;
840906 }
841 if(currentScope) {
907
908 if (currentScope) {
842909 currentScope.$destroy();
843910 currentScope = null;
844911 }
845 if(currentElement) {
846 $animate.leave(currentElement, function() {
847 previousElement = null;
912 if (currentElement) {
913 previousLeaveAnimation = $animate.leave(currentElement);
914 previousLeaveAnimation.then(function() {
915 previousLeaveAnimation = null;
848916 });
849 previousElement = currentElement;
850917 currentElement = null;
851918 }
852919 }
866933 // function is called before linking the content, which would apply child
867934 // directives to non existing elements.
868935 var clone = $transclude(newScope, function(clone) {
869 $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () {
936 $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() {
870937 if (angular.isDefined(autoScrollExp)
871938 && (!autoScrollExp || scope.$eval(autoScrollExp))) {
872939 $anchorScroll();
00 /**
1 * @license AngularJS v1.2.23
2 * (c) 2010-2014 Google, Inc. http://angularjs.org
1 * @license AngularJS v1.4.3
2 * (c) 2010-2015 Google, Inc. http://angularjs.org
33 * License: MIT
44 */
55 (function(window, document, undefined) {'use strict';
2929 * should all be static strings, not variables or general expressions.
3030 *
3131 * @param {string} module The namespace to use for the new minErr instance.
32 * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning
33 * error from returned function, for cases when a particular type of error is useful.
3234 * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance
3335 */
3436
35 function minErr(module) {
36 return function () {
37 var code = arguments[0],
38 prefix = '[' + (module ? module + ':' : '') + code + '] ',
39 template = arguments[1],
40 templateArgs = arguments,
41 stringify = function (obj) {
42 if (typeof obj === 'function') {
43 return obj.toString().replace(/ \{[\s\S]*$/, '');
44 } else if (typeof obj === 'undefined') {
45 return 'undefined';
46 } else if (typeof obj !== 'string') {
47 return JSON.stringify(obj);
48 }
49 return obj;
50 },
51 message, i;
52
53 message = prefix + template.replace(/\{\d+\}/g, function (match) {
54 var index = +match.slice(1, -1), arg;
55
56 if (index + 2 < templateArgs.length) {
57 arg = templateArgs[index + 2];
58 if (typeof arg === 'function') {
59 return arg.toString().replace(/ ?\{[\s\S]*$/, '');
60 } else if (typeof arg === 'undefined') {
61 return 'undefined';
62 } else if (typeof arg !== 'string') {
63 return toJson(arg);
64 }
65 return arg;
66 }
37 function minErr(module, ErrorConstructor) {
38 ErrorConstructor = ErrorConstructor || Error;
39 return function() {
40 var SKIP_INDEXES = 2;
41
42 var templateArgs = arguments,
43 code = templateArgs[0],
44 message = '[' + (module ? module + ':' : '') + code + '] ',
45 template = templateArgs[1],
46 paramPrefix, i;
47
48 message += template.replace(/\{\d+\}/g, function(match) {
49 var index = +match.slice(1, -1),
50 shiftedIndex = index + SKIP_INDEXES;
51
52 if (shiftedIndex < templateArgs.length) {
53 return toDebugString(templateArgs[shiftedIndex]);
54 }
55
6756 return match;
6857 });
6958
70 message = message + '\nhttp://errors.angularjs.org/1.2.23/' +
59 message += '\nhttp://errors.angularjs.org/1.4.3/' +
7160 (module ? module + '/' : '') + code;
72 for (i = 2; i < arguments.length; i++) {
73 message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' +
74 encodeURIComponent(stringify(arguments[i]));
75 }
76
77 return new Error(message);
61
62 for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
63 message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
64 encodeURIComponent(toDebugString(templateArgs[i]));
65 }
66
67 return new ErrorConstructor(message);
7868 };
7969 }
8070
8171 /* We need to tell jshint what variables are being exported */
8272 /* global angular: true,
83 msie: true,
84 jqLite: true,
85 jQuery: true,
86 slice: true,
87 push: true,
88 toString: true,
89 ngMinErr: true,
90 angularModule: true,
91 nodeName_: true,
92 uid: true,
93 VALIDITY_STATE_PROPERTY: true,
94
95 lowercase: true,
96 uppercase: true,
97 manualLowercase: true,
98 manualUppercase: true,
99 nodeName_: true,
100 isArrayLike: true,
101 forEach: true,
102 sortedKeys: true,
103 forEachSorted: true,
104 reverseParams: true,
105 nextUid: true,
106 setHashKey: true,
107 extend: true,
108 int: true,
109 inherit: true,
110 noop: true,
111 identity: true,
112 valueFn: true,
113 isUndefined: true,
114 isDefined: true,
115 isObject: true,
116 isString: true,
117 isNumber: true,
118 isDate: true,
119 isArray: true,
120 isFunction: true,
121 isRegExp: true,
122 isWindow: true,
123 isScope: true,
124 isFile: true,
125 isBlob: true,
126 isBoolean: true,
127 isPromiseLike: true,
128 trim: true,
129 isElement: true,
130 makeMap: true,
131 map: true,
132 size: true,
133 includes: true,
134 indexOf: true,
135 arrayRemove: true,
136 isLeafNode: true,
137 copy: true,
138 shallowCopy: true,
139 equals: true,
140 csp: true,
141 concat: true,
142 sliceArgs: true,
143 bind: true,
144 toJsonReplacer: true,
145 toJson: true,
146 fromJson: true,
147 toBoolean: true,
148 startingTag: true,
149 tryDecodeURIComponent: true,
150 parseKeyValue: true,
151 toKeyValue: true,
152 encodeUriSegment: true,
153 encodeUriQuery: true,
154 angularInit: true,
155 bootstrap: true,
156 snake_case: true,
157 bindJQuery: true,
158 assertArg: true,
159 assertArgFn: true,
160 assertNotHasOwnProperty: true,
161 getter: true,
162 getBlockElements: true,
163 hasOwnProperty: true,
73 msie: true,
74 jqLite: true,
75 jQuery: true,
76 slice: true,
77 splice: true,
78 push: true,
79 toString: true,
80 ngMinErr: true,
81 angularModule: true,
82 uid: true,
83 REGEX_STRING_REGEXP: true,
84 VALIDITY_STATE_PROPERTY: true,
85
86 lowercase: true,
87 uppercase: true,
88 manualLowercase: true,
89 manualUppercase: true,
90 nodeName_: true,
91 isArrayLike: true,
92 forEach: true,
93 forEachSorted: true,
94 reverseParams: true,
95 nextUid: true,
96 setHashKey: true,
97 extend: true,
98 toInt: true,
99 inherit: true,
100 merge: true,
101 noop: true,
102 identity: true,
103 valueFn: true,
104 isUndefined: true,
105 isDefined: true,
106 isObject: true,
107 isBlankObject: true,
108 isString: true,
109 isNumber: true,
110 isDate: true,
111 isArray: true,
112 isFunction: true,
113 isRegExp: true,
114 isWindow: true,
115 isScope: true,
116 isFile: true,
117 isFormData: true,
118 isBlob: true,
119 isBoolean: true,
120 isPromiseLike: true,
121 trim: true,
122 escapeForRegexp: true,
123 isElement: true,
124 makeMap: true,
125 includes: true,
126 arrayRemove: true,
127 copy: true,
128 shallowCopy: true,
129 equals: true,
130 csp: true,
131 jq: true,
132 concat: true,
133 sliceArgs: true,
134 bind: true,
135 toJsonReplacer: true,
136 toJson: true,
137 fromJson: true,
138 convertTimezoneToLocal: true,
139 timezoneToOffset: true,
140 startingTag: true,
141 tryDecodeURIComponent: true,
142 parseKeyValue: true,
143 toKeyValue: true,
144 encodeUriSegment: true,
145 encodeUriQuery: true,
146 angularInit: true,
147 bootstrap: true,
148 getTestability: true,
149 snake_case: true,
150 bindJQuery: true,
151 assertArg: true,
152 assertArgFn: true,
153 assertNotHasOwnProperty: true,
154 getter: true,
155 getBlockNodes: true,
156 hasOwnProperty: true,
157 createMap: true,
158
159 NODE_TYPE_ELEMENT: true,
160 NODE_TYPE_ATTRIBUTE: true,
161 NODE_TYPE_TEXT: true,
162 NODE_TYPE_COMMENT: true,
163 NODE_TYPE_DOCUMENT: true,
164 NODE_TYPE_DOCUMENT_FRAGMENT: true,
164165 */
165166
166167 ////////////////////////////////////
180181 * <div doc-module-components="ng"></div>
181182 */
182183
184 var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
185
183186 // The name of a form control's ValidityState property.
184187 // This is used so that it's possible for internal tests to create mock ValidityStates.
185188 var VALIDITY_STATE_PROPERTY = 'validity';
194197 * @param {string} string String to be converted to lowercase.
195198 * @returns {string} Lowercased string.
196199 */
197 var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;};
200 var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
198201 var hasOwnProperty = Object.prototype.hasOwnProperty;
199202
200203 /**
207210 * @param {string} string String to be converted to uppercase.
208211 * @returns {string} Uppercased string.
209212 */
210 var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;};
213 var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
211214
212215
213216 var manualLowercase = function(s) {
233236 }
234237
235238
236 var /** holds major version number for IE or NaN for real browsers */
237 msie,
239 var
240 msie, // holds major version number for IE, or NaN if UA is not IE.
238241 jqLite, // delay binding since jQuery could be loaded after us.
239242 jQuery, // delay binding
240243 slice = [].slice,
244 splice = [].splice,
241245 push = [].push,
242246 toString = Object.prototype.toString,
247 getPrototypeOf = Object.getPrototypeOf,
243248 ngMinErr = minErr('ng'),
244249
245250 /** @name angular */
246251 angular = window.angular || (window.angular = {}),
247252 angularModule,
248 nodeName_,
249 uid = ['0', '0', '0'];
253 uid = 0;
250254
251255 /**
252 * IE 11 changed the format of the UserAgent string.
253 * See http://msdn.microsoft.com/en-us/library/ms537503.aspx
256 * documentMode is an IE-only property
257 * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
254258 */
255 msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]);
256 if (isNaN(msie)) {
257 msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]);
258 }
259 msie = document.documentMode;
259260
260261
261262 /**
269270 return false;
270271 }
271272
272 var length = obj.length;
273
274 if (obj.nodeType === 1 && length) {
273 // Support: iOS 8.2 (not reproducible in simulator)
274 // "length" in obj used to prevent JIT error (gh-11508)
275 var length = "length" in Object(obj) && obj.length;
276
277 if (obj.nodeType === NODE_TYPE_ELEMENT && length) {
275278 return true;
276279 }
277280
287290 *
288291 * @description
289292 * Invokes the `iterator` function once for each item in `obj` collection, which can be either an
290 * object or an array. The `iterator` function is invoked with `iterator(value, key)`, where `value`
291 * is the value of an object property or an array element and `key` is the object property key or
292 * array element index. Specifying a `context` for the function is optional.
293 * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value`
294 * is the value of an object property or an array element, `key` is the object property key or
295 * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional.
293296 *
294297 * It is worth noting that `.forEach` does not iterate over inherited properties because it filters
295298 * using the `hasOwnProperty` method.
299 *
300 * Unlike ES262's
301 * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18),
302 * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just
303 * return the value provided.
296304 *
297305 ```js
298306 var values = {name: 'misko', gender: 'male'};
308316 * @param {Object=} context Object to become context (`this`) for the iterator function.
309317 * @returns {Object|Array} Reference to `obj`.
310318 */
319
311320 function forEach(obj, iterator, context) {
312 var key;
321 var key, length;
313322 if (obj) {
314323 if (isFunction(obj)) {
315324 for (key in obj) {
316325 // Need to check if hasOwnProperty exists,
317326 // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function
318327 if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) {
319 iterator.call(context, obj[key], key);
328 iterator.call(context, obj[key], key, obj);
320329 }
321330 }
322331 } else if (isArray(obj) || isArrayLike(obj)) {
323 for (key = 0; key < obj.length; key++) {
324 iterator.call(context, obj[key], key);
332 var isPrimitive = typeof obj !== 'object';
333 for (key = 0, length = obj.length; key < length; key++) {
334 if (isPrimitive || key in obj) {
335 iterator.call(context, obj[key], key, obj);
336 }
325337 }
326338 } else if (obj.forEach && obj.forEach !== forEach) {
327 obj.forEach(iterator, context);
328 } else {
339 obj.forEach(iterator, context, obj);
340 } else if (isBlankObject(obj)) {
341 // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
342 for (key in obj) {
343 iterator.call(context, obj[key], key, obj);
344 }
345 } else if (typeof obj.hasOwnProperty === 'function') {
346 // Slow path for objects inheriting Object.prototype, hasOwnProperty check needed
329347 for (key in obj) {
330348 if (obj.hasOwnProperty(key)) {
331 iterator.call(context, obj[key], key);
349 iterator.call(context, obj[key], key, obj);
350 }
351 }
352 } else {
353 // Slow path for objects which do not have a method `hasOwnProperty`
354 for (key in obj) {
355 if (hasOwnProperty.call(obj, key)) {
356 iterator.call(context, obj[key], key, obj);
332357 }
333358 }
334359 }
336361 return obj;
337362 }
338363
339 function sortedKeys(obj) {
340 var keys = [];
341 for (var key in obj) {
342 if (obj.hasOwnProperty(key)) {
343 keys.push(key);
344 }
345 }
346 return keys.sort();
347 }
348
349364 function forEachSorted(obj, iterator, context) {
350 var keys = sortedKeys(obj);
351 for ( var i = 0; i < keys.length; i++) {
365 var keys = Object.keys(obj).sort();
366 for (var i = 0; i < keys.length; i++) {
352367 iterator.call(context, obj[keys[i]], keys[i]);
353368 }
354369 return keys;
365380 }
366381
367382 /**
368 * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
369 * characters such as '012ABC'. The reason why we are not using simply a number counter is that
370 * the number string gets longer over time, and it can also overflow, where as the nextId
371 * will grow much slower, it is a string, and it will never overflow.
372 *
373 * @returns {string} an unique alpha-numeric string
383 * A consistent way of creating unique IDs in angular.
384 *
385 * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before
386 * we hit number precision issues in JavaScript.
387 *
388 * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M
389 *
390 * @returns {number} an unique alpha-numeric string
374391 */
375392 function nextUid() {
376 var index = uid.length;
377 var digit;
378
379 while(index) {
380 index--;
381 digit = uid[index].charCodeAt(0);
382 if (digit == 57 /*'9'*/) {
383 uid[index] = 'A';
384 return uid.join('');
385 }
386 if (digit == 90 /*'Z'*/) {
387 uid[index] = '0';
388 } else {
389 uid[index] = String.fromCharCode(digit + 1);
390 return uid.join('');
391 }
392 }
393 uid.unshift('0');
394 return uid.join('');
393 return ++uid;
395394 }
396395
397396
403402 function setHashKey(obj, h) {
404403 if (h) {
405404 obj.$$hashKey = h;
406 }
407 else {
405 } else {
408406 delete obj.$$hashKey;
409407 }
408 }
409
410
411 function baseExtend(dst, objs, deep) {
412 var h = dst.$$hashKey;
413
414 for (var i = 0, ii = objs.length; i < ii; ++i) {
415 var obj = objs[i];
416 if (!isObject(obj) && !isFunction(obj)) continue;
417 var keys = Object.keys(obj);
418 for (var j = 0, jj = keys.length; j < jj; j++) {
419 var key = keys[j];
420 var src = obj[key];
421
422 if (deep && isObject(src)) {
423 if (isDate(src)) {
424 dst[key] = new Date(src.valueOf());
425 } else {
426 if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
427 baseExtend(dst[key], [src], true);
428 }
429 } else {
430 dst[key] = src;
431 }
432 }
433 }
434
435 setHashKey(dst, h);
436 return dst;
410437 }
411438
412439 /**
416443 * @kind function
417444 *
418445 * @description
419 * Extends the destination object `dst` by copying all of the properties from the `src` object(s)
420 * to `dst`. You can specify multiple `src` objects.
446 * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s)
447 * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so
448 * by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`.
449 *
450 * **Note:** Keep in mind that `angular.extend` does not support recursive merge (deep copy). Use
451 * {@link angular.merge} for this.
421452 *
422453 * @param {Object} dst Destination object.
423454 * @param {...Object} src Source object(s).
424455 * @returns {Object} Reference to `dst`.
425456 */
426457 function extend(dst) {
427 var h = dst.$$hashKey;
428 forEach(arguments, function(obj) {
429 if (obj !== dst) {
430 forEach(obj, function(value, key) {
431 dst[key] = value;
432 });
433 }
434 });
435
436 setHashKey(dst,h);
437 return dst;
458 return baseExtend(dst, slice.call(arguments, 1), false);
438459 }
439460
440 function int(str) {
461
462 /**
463 * @ngdoc function
464 * @name angular.merge
465 * @module ng
466 * @kind function
467 *
468 * @description
469 * Deeply extends the destination object `dst` by copying own enumerable properties from the `src` object(s)
470 * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so
471 * by passing an empty object as the target: `var object = angular.merge({}, object1, object2)`.
472 *
473 * Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source
474 * objects, performing a deep copy.
475 *
476 * @param {Object} dst Destination object.
477 * @param {...Object} src Source object(s).
478 * @returns {Object} Reference to `dst`.
479 */
480 function merge(dst) {
481 return baseExtend(dst, slice.call(arguments, 1), true);
482 }
483
484
485
486 function toInt(str) {
441487 return parseInt(str, 10);
442488 }
443489
444490
445491 function inherit(parent, extra) {
446 return extend(new (extend(function() {}, {prototype:parent}))(), extra);
492 return extend(Object.create(parent), extra);
447493 }
448494
449495 /**
481527 return (transformationFn || angular.identity)(value);
482528 };
483529 ```
530 * @param {*} value to be returned.
531 * @returns {*} the value passed in.
484532 */
485533 function identity($) {return $;}
486534 identity.$inject = [];
487535
488536
489537 function valueFn(value) {return function() {return value;};}
538
539 function hasCustomToString(obj) {
540 return isFunction(obj.toString) && obj.toString !== Object.prototype.toString;
541 }
542
490543
491544 /**
492545 * @ngdoc function
500553 * @param {*} value Reference to check.
501554 * @returns {boolean} True if `value` is undefined.
502555 */
503 function isUndefined(value){return typeof value === 'undefined';}
556 function isUndefined(value) {return typeof value === 'undefined';}
504557
505558
506559 /**
515568 * @param {*} value Reference to check.
516569 * @returns {boolean} True if `value` is defined.
517570 */
518 function isDefined(value){return typeof value !== 'undefined';}
571 function isDefined(value) {return typeof value !== 'undefined';}
519572
520573
521574 /**
531584 * @param {*} value Reference to check.
532585 * @returns {boolean} True if `value` is an `Object` but not `null`.
533586 */
534 function isObject(value){return value != null && typeof value === 'object';}
587 function isObject(value) {
588 // http://jsperf.com/isobject4
589 return value !== null && typeof value === 'object';
590 }
591
592
593 /**
594 * Determine if a value is an object with a null prototype
595 *
596 * @returns {boolean} True if `value` is an `Object` with a null prototype
597 */
598 function isBlankObject(value) {
599 return value !== null && typeof value === 'object' && !getPrototypeOf(value);
600 }
535601
536602
537603 /**
546612 * @param {*} value Reference to check.
547613 * @returns {boolean} True if `value` is a `String`.
548614 */
549 function isString(value){return typeof value === 'string';}
615 function isString(value) {return typeof value === 'string';}
550616
551617
552618 /**
558624 * @description
559625 * Determines if a reference is a `Number`.
560626 *
627 * This includes the "special" numbers `NaN`, `+Infinity` and `-Infinity`.
628 *
629 * If you wish to exclude these then you can use the native
630 * [`isFinite'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite)
631 * method.
632 *
561633 * @param {*} value Reference to check.
562634 * @returns {boolean} True if `value` is a `Number`.
563635 */
564 function isNumber(value){return typeof value === 'number';}
636 function isNumber(value) {return typeof value === 'number';}
565637
566638
567639 /**
593665 * @param {*} value Reference to check.
594666 * @returns {boolean} True if `value` is an `Array`.
595667 */
596 var isArray = (function() {
597 if (!isFunction(Array.isArray)) {
598 return function(value) {
599 return toString.call(value) === '[object Array]';
600 };
601 }
602 return Array.isArray;
603 })();
668 var isArray = Array.isArray;
604669
605670 /**
606671 * @ngdoc function
614679 * @param {*} value Reference to check.
615680 * @returns {boolean} True if `value` is a `Function`.
616681 */
617 function isFunction(value){return typeof value === 'function';}
682 function isFunction(value) {return typeof value === 'function';}
618683
619684
620685 /**
637702 * @returns {boolean} True if `obj` is a window obj.
638703 */
639704 function isWindow(obj) {
640 return obj && obj.document && obj.location && obj.alert && obj.setInterval;
705 return obj && obj.window === obj;
641706 }
642707
643708
651716 }
652717
653718
719 function isFormData(obj) {
720 return toString.call(obj) === '[object FormData]';
721 }
722
723
654724 function isBlob(obj) {
655725 return toString.call(obj) === '[object Blob]';
656726 }
666736 }
667737
668738
669 var trim = (function() {
670 // native trim is way faster: http://jsperf.com/angular-trim-test
671 // but IE doesn't have it... :-(
672 // TODO: we should move this into IE/ES5 polyfill
673 if (!String.prototype.trim) {
674 return function(value) {
675 return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value;
676 };
677 }
678 return function(value) {
679 return isString(value) ? value.trim() : value;
680 };
681 })();
739 var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/;
740 function isTypedArray(value) {
741 return TYPED_ARRAY_REGEXP.test(toString.call(value));
742 }
743
744
745 var trim = function(value) {
746 return isString(value) ? value.trim() : value;
747 };
748
749 // Copied from:
750 // http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021
751 // Prereq: s is a string.
752 var escapeForRegexp = function(s) {
753 return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
754 replace(/\x08/g, '\\x08');
755 };
682756
683757
684758 /**
705779 */
706780 function makeMap(str) {
707781 var obj = {}, items = str.split(","), i;
708 for ( i = 0; i < items.length; i++ )
709 obj[ items[i] ] = true;
782 for (i = 0; i < items.length; i++) {
783 obj[items[i]] = true;
784 }
710785 return obj;
711786 }
712787
713788
714 if (msie < 9) {
715 nodeName_ = function(element) {
716 element = element.nodeName ? element : element[0];
717 return (element.scopeName && element.scopeName != 'HTML')
718 ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName;
719 };
720 } else {
721 nodeName_ = function(element) {
722 return element.nodeName ? element.nodeName : element[0].nodeName;
723 };
789 function nodeName_(element) {
790 return lowercase(element.nodeName || (element[0] && element[0].nodeName));
724791 }
725792
726
727 function map(obj, iterator, context) {
728 var results = [];
729 forEach(obj, function(value, index, list) {
730 results.push(iterator.call(context, value, index, list));
731 });
732 return results;
793 function includes(array, obj) {
794 return Array.prototype.indexOf.call(array, obj) != -1;
733795 }
734796
735
736 /**
737 * @description
738 * Determines the number of elements in an array, the number of properties an object has, or
739 * the length of a string.
740 *
741 * Note: This function is used to augment the Object type in Angular expressions. See
742 * {@link angular.Object} for more information about Angular arrays.
743 *
744 * @param {Object|Array|string} obj Object, array, or string to inspect.
745 * @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object
746 * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array.
747 */
748 function size(obj, ownPropsOnly) {
749 var count = 0, key;
750
751 if (isArray(obj) || isString(obj)) {
752 return obj.length;
753 } else if (isObject(obj)) {
754 for (key in obj)
755 if (!ownPropsOnly || obj.hasOwnProperty(key))
756 count++;
797 function arrayRemove(array, value) {
798 var index = array.indexOf(value);
799 if (index >= 0) {
800 array.splice(index, 1);
757801 }
758
759 return count;
760 }
761
762
763 function includes(array, obj) {
764 return indexOf(array, obj) != -1;
765 }
766
767 function indexOf(array, obj) {
768 if (array.indexOf) return array.indexOf(obj);
769
770 for (var i = 0; i < array.length; i++) {
771 if (obj === array[i]) return i;
772 }
773 return -1;
774 }
775
776 function arrayRemove(array, value) {
777 var index = indexOf(array, value);
778 if (index >=0)
779 array.splice(index, 1);
780 return value;
781 }
782
783 function isLeafNode (node) {
784 if (node) {
785 switch (node.nodeName) {
786 case "OPTION":
787 case "PRE":
788 case "TITLE":
789 return true;
790 }
791 }
792 return false;
802 return index;
793803 }
794804
795805 /**
802812 * Creates a deep copy of `source`, which should be an object or an array.
803813 *
804814 * * If no destination is supplied, a copy of the object or array is created.
805 * * If a destination is provided, all of its elements (for array) or properties (for objects)
815 * * If a destination is provided, all of its elements (for arrays) or properties (for objects)
806816 * are deleted and then all elements/properties from the source are copied to it.
807817 * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned.
808818 * * If `source` is identical to 'destination' an exception will be thrown.
855865 throw ngMinErr('cpws',
856866 "Can't copy! Making copies of Window or Scope instances is not supported.");
857867 }
868 if (isTypedArray(destination)) {
869 throw ngMinErr('cpta',
870 "Can't copy! TypedArray destination cannot be mutated.");
871 }
858872
859873 if (!destination) {
860874 destination = source;
861 if (source) {
875 if (isObject(source)) {
876 var index;
877 if (stackSource && (index = stackSource.indexOf(source)) !== -1) {
878 return stackDest[index];
879 }
880
881 // TypedArray, Date and RegExp have specific copy functionality and must be
882 // pushed onto the stack before returning.
883 // Array and other objects create the base object and recurse to copy child
884 // objects. The array/object will be pushed onto the stack when recursed.
862885 if (isArray(source)) {
863 destination = copy(source, [], stackSource, stackDest);
886 return copy(source, [], stackSource, stackDest);
887 } else if (isTypedArray(source)) {
888 destination = new source.constructor(source);
864889 } else if (isDate(source)) {
865890 destination = new Date(source.getTime());
866891 } else if (isRegExp(source)) {
867892 destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
868893 destination.lastIndex = source.lastIndex;
869 } else if (isObject(source)) {
870 destination = copy(source, {}, stackSource, stackDest);
894 } else {
895 var emptyObject = Object.create(getPrototypeOf(source));
896 return copy(source, emptyObject, stackSource, stackDest);
897 }
898
899 if (stackDest) {
900 stackSource.push(source);
901 stackDest.push(destination);
871902 }
872903 }
873904 } else {
878909 stackDest = stackDest || [];
879910
880911 if (isObject(source)) {
881 var index = indexOf(stackSource, source);
882 if (index !== -1) return stackDest[index];
883
884912 stackSource.push(source);
885913 stackDest.push(destination);
886914 }
887915
888 var result;
916 var result, key;
889917 if (isArray(source)) {
890918 destination.length = 0;
891 for ( var i = 0; i < source.length; i++) {
892 result = copy(source[i], null, stackSource, stackDest);
893 if (isObject(source[i])) {
894 stackSource.push(source[i]);
895 stackDest.push(result);
896 }
897 destination.push(result);
919 for (var i = 0; i < source.length; i++) {
920 destination.push(copy(source[i], null, stackSource, stackDest));
898921 }
899922 } else {
900923 var h = destination.$$hashKey;
905928 delete destination[key];
906929 });
907930 }
908 for ( var key in source) {
909 result = copy(source[key], null, stackSource, stackDest);
910 if (isObject(source[key])) {
911 stackSource.push(source[key]);
912 stackDest.push(result);
931 if (isBlankObject(source)) {
932 // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
933 for (key in source) {
934 destination[key] = copy(source[key], null, stackSource, stackDest);
913935 }
914 destination[key] = result;
936 } else if (source && typeof source.hasOwnProperty === 'function') {
937 // Slow path, which must rely on hasOwnProperty
938 for (key in source) {
939 if (source.hasOwnProperty(key)) {
940 destination[key] = copy(source[key], null, stackSource, stackDest);
941 }
942 }
943 } else {
944 // Slowest path --- hasOwnProperty can't be called as a method
945 for (key in source) {
946 if (hasOwnProperty.call(source, key)) {
947 destination[key] = copy(source[key], null, stackSource, stackDest);
948 }
949 }
915950 }
916951 setHashKey(destination,h);
917952 }
918
919953 }
920954 return destination;
921955 }
922956
923957 /**
924 * Creates a shallow copy of an object, an array or a primitive
958 * Creates a shallow copy of an object, an array or a primitive.
959 *
960 * Assumes that there are no proto properties for objects.
925961 */
926962 function shallowCopy(src, dst) {
927963 if (isArray(src)) {
928964 dst = dst || [];
929965
930 for ( var i = 0; i < src.length; i++) {
966 for (var i = 0, ii = src.length; i < ii; i++) {
931967 dst[i] = src[i];
932968 }
933969 } else if (isObject(src)) {
934970 dst = dst || {};
935971
936972 for (var key in src) {
937 if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
973 if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) {
938974 dst[key] = src[key];
939975 }
940976 }
9831019 if (isArray(o1)) {
9841020 if (!isArray(o2)) return false;
9851021 if ((length = o1.length) == o2.length) {
986 for(key=0; key<length; key++) {
1022 for (key = 0; key < length; key++) {
9871023 if (!equals(o1[key], o2[key])) return false;
9881024 }
9891025 return true;
9901026 }
9911027 } else if (isDate(o1)) {
9921028 if (!isDate(o2)) return false;
993 return (isNaN(o1.getTime()) && isNaN(o2.getTime())) || (o1.getTime() === o2.getTime());
994 } else if (isRegExp(o1) && isRegExp(o2)) {
995 return o1.toString() == o2.toString();
1029 return equals(o1.getTime(), o2.getTime());
1030 } else if (isRegExp(o1)) {
1031 return isRegExp(o2) ? o1.toString() == o2.toString() : false;
9961032 } else {
997 if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2)) return false;
998 keySet = {};
999 for(key in o1) {
1033 if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
1034 isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
1035 keySet = createMap();
1036 for (key in o1) {
10001037 if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
10011038 if (!equals(o1[key], o2[key])) return false;
10021039 keySet[key] = true;
10031040 }
1004 for(key in o2) {
1005 if (!keySet.hasOwnProperty(key) &&
1041 for (key in o2) {
1042 if (!(key in keySet) &&
10061043 key.charAt(0) !== '$' &&
10071044 o2[key] !== undefined &&
10081045 !isFunction(o2[key])) return false;
10331070 return (csp.isActive_ = active);
10341071 };
10351072
1036
1073 /**
1074 * @ngdoc directive
1075 * @module ng
1076 * @name ngJq
1077 *
1078 * @element ANY
1079 * @param {string=} ngJq the name of the library available under `window`
1080 * to be used for angular.element
1081 * @description
1082 * Use this directive to force the angular.element library. This should be
1083 * used to force either jqLite by leaving ng-jq blank or setting the name of
1084 * the jquery variable under window (eg. jQuery).
1085 *
1086 * Since angular looks for this directive when it is loaded (doesn't wait for the
1087 * DOMContentLoaded event), it must be placed on an element that comes before the script
1088 * which loads angular. Also, only the first instance of `ng-jq` will be used and all
1089 * others ignored.
1090 *
1091 * @example
1092 * This example shows how to force jqLite using the `ngJq` directive to the `html` tag.
1093 ```html
1094 <!doctype html>
1095 <html ng-app ng-jq>
1096 ...
1097 ...
1098 </html>
1099 ```
1100 * @example
1101 * This example shows how to use a jQuery based library of a different name.
1102 * The library name must be available at the top most 'window'.
1103 ```html
1104 <!doctype html>
1105 <html ng-app ng-jq="jQueryLib">
1106 ...
1107 ...
1108 </html>
1109 ```
1110 */
1111 var jq = function() {
1112 if (isDefined(jq.name_)) return jq.name_;
1113 var el;
1114 var i, ii = ngAttrPrefixes.length, prefix, name;
1115 for (i = 0; i < ii; ++i) {
1116 prefix = ngAttrPrefixes[i];
1117 if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
1118 name = el.getAttribute(prefix + 'jq');
1119 break;
1120 }
1121 }
1122
1123 return (jq.name_ = name);
1124 };
10371125
10381126 function concat(array1, array2, index) {
10391127 return array1.concat(slice.call(array2, index));
10691157 return curryArgs.length
10701158 ? function() {
10711159 return arguments.length
1072 ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0)))
1160 ? fn.apply(self, concat(curryArgs, arguments, 0))
10731161 : fn.apply(self, curryArgs);
10741162 }
10751163 : function() {
10871175 function toJsonReplacer(key, value) {
10881176 var val = value;
10891177
1090 if (typeof key === 'string' && key.charAt(0) === '$') {
1178 if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') {
10911179 val = undefined;
10921180 } else if (isWindow(value)) {
10931181 val = '$WINDOW';
11081196 * @kind function
11091197 *
11101198 * @description
1111 * Serializes input into a JSON-formatted string. Properties with leading $ characters will be
1199 * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be
11121200 * stripped since angular uses this notation internally.
11131201 *
11141202 * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON.
1115 * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace.
1203 * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace.
1204 * If set to an integer, the JSON output will contain that many spaces per indentation.
11161205 * @returns {string|undefined} JSON-ified string representing `obj`.
11171206 */
11181207 function toJson(obj, pretty) {
11191208 if (typeof obj === 'undefined') return undefined;
1120 return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null);
1209 if (!isNumber(pretty)) {
1210 pretty = pretty ? 2 : null;
1211 }
1212 return JSON.stringify(obj, toJsonReplacer, pretty);
11211213 }
11221214
11231215
11311223 * Deserializes a JSON string.
11321224 *
11331225 * @param {string} json JSON string to deserialize.
1134 * @returns {Object|Array|string|number} Deserialized thingy.
1226 * @returns {Object|Array|string|number} Deserialized JSON string.
11351227 */
11361228 function fromJson(json) {
11371229 return isString(json)
11401232 }
11411233
11421234
1143 function toBoolean(value) {
1144 if (typeof value === 'function') {
1145 value = true;
1146 } else if (value && value.length !== 0) {
1147 var v = lowercase("" + value);
1148 value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]');
1149 } else {
1150 value = false;
1151 }
1152 return value;
1235 function timezoneToOffset(timezone, fallback) {
1236 var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
1237 return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
11531238 }
1239
1240
1241 function addDateMinutes(date, minutes) {
1242 date = new Date(date.getTime());
1243 date.setMinutes(date.getMinutes() + minutes);
1244 return date;
1245 }
1246
1247
1248 function convertTimezoneToLocal(date, timezone, reverse) {
1249 reverse = reverse ? -1 : 1;
1250 var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
1251 return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset()));
1252 }
1253
11541254
11551255 /**
11561256 * @returns {string} Returns the string representation of the element.
11611261 // turns out IE does not let you set .html() on elements which
11621262 // are not allowed to have children. So we just ignore it.
11631263 element.empty();
1164 } catch(e) {}
1165 // As Per DOM Standards
1166 var TEXT_NODE = 3;
1264 } catch (e) {}
11671265 var elemHtml = jqLite('<div>').append(element).html();
11681266 try {
1169 return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) :
1267 return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) :
11701268 elemHtml.
11711269 match(/^(<[^>]+>)/)[1].
11721270 replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); });
1173 } catch(e) {
1271 } catch (e) {
11741272 return lowercase(elemHtml);
11751273 }
11761274
11901288 function tryDecodeURIComponent(value) {
11911289 try {
11921290 return decodeURIComponent(value);
1193 } catch(e) {
1291 } catch (e) {
11941292 // Ignore any invalid uri component
11951293 }
11961294 }
12031301 function parseKeyValue(/**string*/keyValue) {
12041302 var obj = {}, key_value, key;
12051303 forEach((keyValue || "").split('&'), function(keyValue) {
1206 if ( keyValue ) {
1304 if (keyValue) {
12071305 key_value = keyValue.replace(/\+/g,'%20').split('=');
12081306 key = tryDecodeURIComponent(key_value[0]);
1209 if ( isDefined(key) ) {
1307 if (isDefined(key)) {
12101308 var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
12111309 if (!hasOwnProperty.call(obj, key)) {
12121310 obj[key] = val;
1213 } else if(isArray(obj[key])) {
1311 } else if (isArray(obj[key])) {
12141312 obj[key].push(val);
12151313 } else {
12161314 obj[key] = [obj[key],val];
12741372 replace(/%3A/gi, ':').
12751373 replace(/%24/g, '$').
12761374 replace(/%2C/gi, ',').
1375 replace(/%3B/gi, ';').
12771376 replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
12781377 }
12791378
1379 var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-'];
1380
1381 function getNgAttribute(element, ngAttr) {
1382 var attr, i, ii = ngAttrPrefixes.length;
1383 for (i = 0; i < ii; ++i) {
1384 attr = ngAttrPrefixes[i] + ngAttr;
1385 if (isString(attr = element.getAttribute(attr))) {
1386 return attr;
1387 }
1388 }
1389 return null;
1390 }
12801391
12811392 /**
12821393 * @ngdoc directive
12861397 * @element ANY
12871398 * @param {angular.Module} ngApp an optional application
12881399 * {@link angular.module module} name to load.
1400 * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be
1401 * created in "strict-di" mode. This means that the application will fail to invoke functions which
1402 * do not use explicit function annotation (and are thus unsuitable for minification), as described
1403 * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in
1404 * tracking down the root of these bugs.
12891405 *
12901406 * @description
12911407 *
12991415 * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other.
13001416 *
13011417 * You can specify an **AngularJS module** to be used as the root module for the application. This
1302 * module will be loaded into the {@link auto.$injector} when the application is bootstrapped and
1418 * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It
13031419 * should contain the application code needed or have dependencies on other modules that will
13041420 * contain the code. See {@link angular.module} for more information.
13051421 *
13071423 * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}`
13081424 * would not be resolved to `3`.
13091425 *
1310 * `ngApp` is the easiest, and most common, way to bootstrap an application.
1426 * `ngApp` is the easiest, and most common way to bootstrap an application.
13111427 *
13121428 <example module="ngAppDemo">
13131429 <file name="index.html">
13231439 </file>
13241440 </example>
13251441 *
1442 * Using `ngStrictDi`, you would see something like this:
1443 *
1444 <example ng-app-included="true">
1445 <file name="index.html">
1446 <div ng-app="ngAppStrictDemo" ng-strict-di>
1447 <div ng-controller="GoodController1">
1448 I can add: {{a}} + {{b}} = {{ a+b }}
1449
1450 <p>This renders because the controller does not fail to
1451 instantiate, by using explicit annotation style (see
1452 script.js for details)
1453 </p>
1454 </div>
1455
1456 <div ng-controller="GoodController2">
1457 Name: <input ng-model="name"><br />
1458 Hello, {{name}}!
1459
1460 <p>This renders because the controller does not fail to
1461 instantiate, by using explicit annotation style
1462 (see script.js for details)
1463 </p>
1464 </div>
1465
1466 <div ng-controller="BadController">
1467 I can add: {{a}} + {{b}} = {{ a+b }}
1468
1469 <p>The controller could not be instantiated, due to relying
1470 on automatic function annotations (which are disabled in
1471 strict mode). As such, the content of this section is not
1472 interpolated, and there should be an error in your web console.
1473 </p>
1474 </div>
1475 </div>
1476 </file>
1477 <file name="script.js">
1478 angular.module('ngAppStrictDemo', [])
1479 // BadController will fail to instantiate, due to relying on automatic function annotation,
1480 // rather than an explicit annotation
1481 .controller('BadController', function($scope) {
1482 $scope.a = 1;
1483 $scope.b = 2;
1484 })
1485 // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated,
1486 // due to using explicit annotations using the array style and $inject property, respectively.
1487 .controller('GoodController1', ['$scope', function($scope) {
1488 $scope.a = 1;
1489 $scope.b = 2;
1490 }])
1491 .controller('GoodController2', GoodController2);
1492 function GoodController2($scope) {
1493 $scope.name = "World";
1494 }
1495 GoodController2.$inject = ['$scope'];
1496 </file>
1497 <file name="style.css">
1498 div[ng-controller] {
1499 margin-bottom: 1em;
1500 -webkit-border-radius: 4px;
1501 border-radius: 4px;
1502 border: 1px solid;
1503 padding: .5em;
1504 }
1505 div[ng-controller^=Good] {
1506 border-color: #d6e9c6;
1507 background-color: #dff0d8;
1508 color: #3c763d;
1509 }
1510 div[ng-controller^=Bad] {
1511 border-color: #ebccd1;
1512 background-color: #f2dede;
1513 color: #a94442;
1514 margin-bottom: 0;
1515 }
1516 </file>
1517 </example>
13261518 */
13271519 function angularInit(element, bootstrap) {
1328 var elements = [element],
1329 appElement,
1520 var appElement,
13301521 module,
1331 names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'],
1332 NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;
1333
1334 function append(element) {
1335 element && elements.push(element);
1336 }
1337
1338 forEach(names, function(name) {
1339 names[name] = true;
1340 append(document.getElementById(name));
1341 name = name.replace(':', '\\:');
1342 if (element.querySelectorAll) {
1343 forEach(element.querySelectorAll('.' + name), append);
1344 forEach(element.querySelectorAll('.' + name + '\\:'), append);
1345 forEach(element.querySelectorAll('[' + name + ']'), append);
1522 config = {};
1523
1524 // The element `element` has priority over any other element
1525 forEach(ngAttrPrefixes, function(prefix) {
1526 var name = prefix + 'app';
1527
1528 if (!appElement && element.hasAttribute && element.hasAttribute(name)) {
1529 appElement = element;
1530 module = element.getAttribute(name);
13461531 }
13471532 });
1348
1349 forEach(elements, function(element) {
1350 if (!appElement) {
1351 var className = ' ' + element.className + ' ';
1352 var match = NG_APP_CLASS_REGEXP.exec(className);
1353 if (match) {
1354 appElement = element;
1355 module = (match[2] || '').replace(/\s+/g, ',');
1356 } else {
1357 forEach(element.attributes, function(attr) {
1358 if (!appElement && names[attr.name]) {
1359 appElement = element;
1360 module = attr.value;
1361 }
1362 });
1363 }
1533 forEach(ngAttrPrefixes, function(prefix) {
1534 var name = prefix + 'app';
1535 var candidate;
1536
1537 if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) {
1538 appElement = candidate;
1539 module = candidate.getAttribute(name);
13641540 }
13651541 });
13661542 if (appElement) {
1367 bootstrap(appElement, module ? [module] : []);
1543 config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
1544 bootstrap(appElement, module ? [module] : [], config);
13681545 }
13691546 }
13701547
13771554 *
13781555 * See: {@link guide/bootstrap Bootstrap}
13791556 *
1380 * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually.
1557 * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually.
13811558 * They must use {@link ng.directive:ngApp ngApp}.
13821559 *
13831560 * Angular will detect if it has been loaded into the browser more than once and only allow the
13851562 * each of the subsequent scripts. This prevents strange results in applications, where otherwise
13861563 * multiple instances of Angular try to work on the DOM.
13871564 *
1388 * <example name="multi-bootstrap" module="multi-bootstrap">
1389 * <file name="index.html">
1390 * <script src="../../../angular.js"></script>
1391 * <div ng-controller="BrokenTable">
1392 * <table>
1393 * <tr>
1394 * <th ng-repeat="heading in headings">{{heading}}</th>
1395 * </tr>
1396 * <tr ng-repeat="filling in fillings">
1397 * <td ng-repeat="fill in filling">{{fill}}</td>
1398 * </tr>
1399 * </table>
1565 * ```html
1566 * <!doctype html>
1567 * <html>
1568 * <body>
1569 * <div ng-controller="WelcomeController">
1570 * {{greeting}}
14001571 * </div>
1401 * </file>
1402 * <file name="controller.js">
1403 * var app = angular.module('multi-bootstrap', [])
1404 *
1405 * .controller('BrokenTable', function($scope) {
1406 * $scope.headings = ['One', 'Two', 'Three'];
1407 * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]];
1408 * });
1409 * </file>
1410 * <file name="protractor.js" type="protractor">
1411 * it('should only insert one table cell for each item in $scope.fillings', function() {
1412 * expect(element.all(by.css('td')).count())
1413 * .toBe(9);
1414 * });
1415 * </file>
1416 * </example>
1572 *
1573 * <script src="angular.js"></script>
1574 * <script>
1575 * var app = angular.module('demo', [])
1576 * .controller('WelcomeController', function($scope) {
1577 * $scope.greeting = 'Welcome!';
1578 * });
1579 * angular.bootstrap(document, ['demo']);
1580 * </script>
1581 * </body>
1582 * </html>
1583 * ```
14171584 *
14181585 * @param {DOMElement} element DOM element which is the root of angular application.
14191586 * @param {Array<String|Function|Array>=} modules an array of modules to load into the application.
14201587 * Each item in the array should be the name of a predefined module or a (DI annotated)
1421 * function that will be invoked by the injector as a run block.
1588 * function that will be invoked by the injector as a `config` block.
14221589 * See: {@link angular.module modules}
1590 * @param {Object=} config an object for defining configuration options for the application. The
1591 * following keys are supported:
1592 *
1593 * * `strictDi` - disable automatic function annotation for the application. This is meant to
1594 * assist in finding bugs which break minified code. Defaults to `false`.
1595 *
14231596 * @returns {auto.$injector} Returns the newly created injector for this app.
14241597 */
1425 function bootstrap(element, modules) {
1598 function bootstrap(element, modules, config) {
1599 if (!isObject(config)) config = {};
1600 var defaultConfig = {
1601 strictDi: false
1602 };
1603 config = extend(defaultConfig, config);
14261604 var doBootstrap = function() {
14271605 element = jqLite(element);
14281606
14391617 modules.unshift(['$provide', function($provide) {
14401618 $provide.value('$rootElement', element);
14411619 }]);
1620
1621 if (config.debugInfoEnabled) {
1622 // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`.
1623 modules.push(['$compileProvider', function($compileProvider) {
1624 $compileProvider.debugInfoEnabled(true);
1625 }]);
1626 }
1627
14421628 modules.unshift('ng');
1443 var injector = createInjector(modules);
1444 injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
1445 function(scope, element, compile, injector, animate) {
1629 var injector = createInjector(modules, config.strictDi);
1630 injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
1631 function bootstrapApply(scope, element, compile, injector) {
14461632 scope.$apply(function() {
14471633 element.data('$injector', injector);
14481634 compile(element)(scope);
14521638 return injector;
14531639 };
14541640
1641 var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/;
14551642 var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/;
1643
1644 if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) {
1645 config.debugInfoEnabled = true;
1646 window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, '');
1647 }
14561648
14571649 if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) {
14581650 return doBootstrap();
14631655 forEach(extraModules, function(module) {
14641656 modules.push(module);
14651657 });
1466 doBootstrap();
1658 return doBootstrap();
14671659 };
1660
1661 if (isFunction(angular.resumeDeferredBootstrap)) {
1662 angular.resumeDeferredBootstrap();
1663 }
1664 }
1665
1666 /**
1667 * @ngdoc function
1668 * @name angular.reloadWithDebugInfo
1669 * @module ng
1670 * @description
1671 * Use this function to reload the current application with debug information turned on.
1672 * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`.
1673 *
1674 * See {@link ng.$compileProvider#debugInfoEnabled} for more.
1675 */
1676 function reloadWithDebugInfo() {
1677 window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name;
1678 window.location.reload();
1679 }
1680
1681 /**
1682 * @name angular.getTestability
1683 * @module ng
1684 * @description
1685 * Get the testability service for the instance of Angular on the given
1686 * element.
1687 * @param {DOMElement} element DOM element which is the root of angular application.
1688 */
1689 function getTestability(rootElement) {
1690 var injector = angular.element(rootElement).injector();
1691 if (!injector) {
1692 throw ngMinErr('test',
1693 'no injector found for element argument to getTestability');
1694 }
1695 return injector.get('$$testability');
14681696 }
14691697
14701698 var SNAKE_CASE_REGEXP = /[A-Z]/g;
14751703 });
14761704 }
14771705
1706 var bindJQueryFired = false;
1707 var skipDestroyOnNextJQueryCleanData;
14781708 function bindJQuery() {
1709 var originalCleanData;
1710
1711 if (bindJQueryFired) {
1712 return;
1713 }
1714
14791715 // bind to jQuery if present;
1480 jQuery = window.jQuery;
1716 var jqName = jq();
1717 jQuery = window.jQuery; // use default jQuery.
1718 if (isDefined(jqName)) { // `ngJq` present
1719 jQuery = jqName === null ? undefined : window[jqName]; // if empty; use jqLite. if not empty, use jQuery specified by `ngJq`.
1720 }
1721
14811722 // Use jQuery if it exists with proper functionality, otherwise default to us.
1482 // Angular 1.2+ requires jQuery 1.7.1+ for on()/off() support.
1723 // Angular 1.2+ requires jQuery 1.7+ for on()/off() support.
1724 // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older
1725 // versions. It will not work for sure with jQuery <1.7, though.
14831726 if (jQuery && jQuery.fn.on) {
14841727 jqLite = jQuery;
14851728 extend(jQuery.fn, {
14891732 injector: JQLitePrototype.injector,
14901733 inheritedData: JQLitePrototype.inheritedData
14911734 });
1492 // Method signature:
1493 // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
1494 jqLitePatchJQueryRemove('remove', true, true, false);
1495 jqLitePatchJQueryRemove('empty', false, false, false);
1496 jqLitePatchJQueryRemove('html', false, false, true);
1735
1736 // All nodes removed from the DOM via various jQuery APIs like .remove()
1737 // are passed through jQuery.cleanData. Monkey-patch this method to fire
1738 // the $destroy event on all removed nodes.
1739 originalCleanData = jQuery.cleanData;
1740 jQuery.cleanData = function(elems) {
1741 var events;
1742 if (!skipDestroyOnNextJQueryCleanData) {
1743 for (var i = 0, elem; (elem = elems[i]) != null; i++) {
1744 events = jQuery._data(elem, "events");
1745 if (events && events.$destroy) {
1746 jQuery(elem).triggerHandler('$destroy');
1747 }
1748 }
1749 } else {
1750 skipDestroyOnNextJQueryCleanData = false;
1751 }
1752 originalCleanData(elems);
1753 };
14971754 } else {
14981755 jqLite = JQLite;
14991756 }
1757
15001758 angular.element = jqLite;
1759
1760 // Prevent double-proxying.
1761 bindJQueryFired = true;
15011762 }
15021763
15031764 /**
15611822 /**
15621823 * Return the DOM siblings between the first and last node in the given array.
15631824 * @param {Array} array like object
1564 * @returns {DOMElement} object containing the elements
1825 * @returns {jqLite} jqLite collection containing the nodes
15651826 */
1566 function getBlockElements(nodes) {
1567 var startNode = nodes[0],
1568 endNode = nodes[nodes.length - 1];
1569 if (startNode === endNode) {
1570 return jqLite(startNode);
1571 }
1572
1573 var element = startNode;
1574 var elements = [element];
1827 function getBlockNodes(nodes) {
1828 // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original
1829 // collection, otherwise update the original collection.
1830 var node = nodes[0];
1831 var endNode = nodes[nodes.length - 1];
1832 var blockNodes = [node];
15751833
15761834 do {
1577 element = element.nextSibling;
1578 if (!element) break;
1579 elements.push(element);
1580 } while (element !== endNode);
1581
1582 return jqLite(elements);
1835 node = node.nextSibling;
1836 if (!node) break;
1837 blockNodes.push(node);
1838 } while (node !== endNode);
1839
1840 return jqLite(blockNodes);
15831841 }
1842
1843
1844 /**
1845 * Creates a new object without a prototype. This object is useful for lookup without having to
1846 * guard against prototypically inherited properties via hasOwnProperty.
1847 *
1848 * Related micro-benchmarks:
1849 * - http://jsperf.com/object-create2
1850 * - http://jsperf.com/proto-map-lookup/2
1851 * - http://jsperf.com/for-in-vs-object-keys2
1852 *
1853 * @returns {Object}
1854 */
1855 function createMap() {
1856 return Object.create(null);
1857 }
1858
1859 var NODE_TYPE_ELEMENT = 1;
1860 var NODE_TYPE_ATTRIBUTE = 2;
1861 var NODE_TYPE_TEXT = 3;
1862 var NODE_TYPE_COMMENT = 8;
1863 var NODE_TYPE_DOCUMENT = 9;
1864 var NODE_TYPE_DOCUMENT_FRAGMENT = 11;
15841865
15851866 /**
15861867 * @ngdoc type
16821963 var invokeQueue = [];
16831964
16841965 /** @type {!Array.<Function>} */
1966 var configBlocks = [];
1967
1968 /** @type {!Array.<Function>} */
16851969 var runBlocks = [];
16861970
1687 var config = invokeLater('$injector', 'invoke');
1971 var config = invokeLater('$injector', 'invoke', 'push', configBlocks);
16881972
16891973 /** @type {angular.Module} */
16901974 var moduleInstance = {
16911975 // Private state
16921976 _invokeQueue: invokeQueue,
1977 _configBlocks: configBlocks,
16931978 _runBlocks: runBlocks,
16941979
16951980 /**
17242009 * @description
17252010 * See {@link auto.$provide#provider $provide.provider()}.
17262011 */
1727 provider: invokeLater('$provide', 'provider'),
2012 provider: invokeLaterAndSetModuleName('$provide', 'provider'),
17282013
17292014 /**
17302015 * @ngdoc method
17352020 * @description
17362021 * See {@link auto.$provide#factory $provide.factory()}.
17372022 */
1738 factory: invokeLater('$provide', 'factory'),
2023 factory: invokeLaterAndSetModuleName('$provide', 'factory'),
17392024
17402025 /**
17412026 * @ngdoc method
17462031 * @description
17472032 * See {@link auto.$provide#service $provide.service()}.
17482033 */
1749 service: invokeLater('$provide', 'service'),
2034 service: invokeLaterAndSetModuleName('$provide', 'service'),
17502035
17512036 /**
17522037 * @ngdoc method
17702055 * See {@link auto.$provide#constant $provide.constant()}.
17712056 */
17722057 constant: invokeLater('$provide', 'constant', 'unshift'),
2058
2059 /**
2060 * @ngdoc method
2061 * @name angular.Module#decorator
2062 * @module ng
2063 * @param {string} The name of the service to decorate.
2064 * @param {Function} This function will be invoked when the service needs to be
2065 * instantiated and should return the decorated service instance.
2066 * @description
2067 * See {@link auto.$provide#decorator $provide.decorator()}.
2068 */
2069 decorator: invokeLaterAndSetModuleName('$provide', 'decorator'),
17732070
17742071 /**
17752072 * @ngdoc method
17842081 *
17852082 *
17862083 * Defines an animation hook that can be later used with
1787 * {@link ngAnimate.$animate $animate} service and directives that use this service.
2084 * {@link $animate $animate} service and directives that use this service.
17882085 *
17892086 * ```js
17902087 * module.animation('.animation-name', function($inject1, $inject2) {
18002097 * })
18012098 * ```
18022099 *
1803 * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and
2100 * See {@link ng.$animateProvider#register $animateProvider.register()} and
18042101 * {@link ngAnimate ngAnimate module} for more information.
18052102 */
1806 animation: invokeLater('$animateProvider', 'register'),
2103 animation: invokeLaterAndSetModuleName('$animateProvider', 'register'),
18072104
18082105 /**
18092106 * @ngdoc method
18102107 * @name angular.Module#filter
18112108 * @module ng
1812 * @param {string} name Filter name.
2109 * @param {string} name Filter name - this must be a valid angular expression identifier
18132110 * @param {Function} filterFactory Factory function for creating new instance of filter.
18142111 * @description
18152112 * See {@link ng.$filterProvider#register $filterProvider.register()}.
2113 *
2114 * <div class="alert alert-warning">
2115 * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
2116 * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
2117 * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
2118 * (`myapp_subsection_filterx`).
2119 * </div>
18162120 */
1817 filter: invokeLater('$filterProvider', 'register'),
2121 filter: invokeLaterAndSetModuleName('$filterProvider', 'register'),
18182122
18192123 /**
18202124 * @ngdoc method
18262130 * @description
18272131 * See {@link ng.$controllerProvider#register $controllerProvider.register()}.
18282132 */
1829 controller: invokeLater('$controllerProvider', 'register'),
2133 controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'),
18302134
18312135 /**
18322136 * @ngdoc method
18392143 * @description
18402144 * See {@link ng.$compileProvider#directive $compileProvider.directive()}.
18412145 */
1842 directive: invokeLater('$compileProvider', 'directive'),
2146 directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'),
18432147
18442148 /**
18452149 * @ngdoc method
18502154 * @description
18512155 * Use this method to register work which needs to be performed on module loading.
18522156 * For more about how to configure services, see
1853 * {@link providers#providers_provider-recipe Provider Recipe}.
2157 * {@link providers#provider-recipe Provider Recipe}.
18542158 */
18552159 config: config,
18562160
18742178 config(configFn);
18752179 }
18762180
1877 return moduleInstance;
2181 return moduleInstance;
18782182
18792183 /**
18802184 * @param {string} provider
18822186 * @param {String=} insertMethod
18832187 * @returns {angular.Module}
18842188 */
1885 function invokeLater(provider, method, insertMethod) {
2189 function invokeLater(provider, method, insertMethod, queue) {
2190 if (!queue) queue = invokeQueue;
18862191 return function() {
1887 invokeQueue[insertMethod || 'push']([provider, method, arguments]);
2192 queue[insertMethod || 'push']([provider, method, arguments]);
2193 return moduleInstance;
2194 };
2195 }
2196
2197 /**
2198 * @param {string} provider
2199 * @param {string} method
2200 * @returns {angular.Module}
2201 */
2202 function invokeLaterAndSetModuleName(provider, method) {
2203 return function(recipeName, factoryFunction) {
2204 if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
2205 invokeQueue.push([provider, method, arguments]);
18882206 return moduleInstance;
18892207 };
18902208 }
18942212
18952213 }
18962214
2215 /* global: toDebugString: true */
2216
2217 function serializeObject(obj) {
2218 var seen = [];
2219
2220 return JSON.stringify(obj, function(key, val) {
2221 val = toJsonReplacer(key, val);
2222 if (isObject(val)) {
2223
2224 if (seen.indexOf(val) >= 0) return '<<already seen>>';
2225
2226 seen.push(val);
2227 }
2228 return val;
2229 });
2230 }
2231
2232 function toDebugString(obj) {
2233 if (typeof obj === 'function') {
2234 return obj.toString().replace(/ \{[\s\S]*$/, '');
2235 } else if (typeof obj === 'undefined') {
2236 return 'undefined';
2237 } else if (typeof obj !== 'string') {
2238 return serializeObject(obj);
2239 }
2240 return obj;
2241 }
2242
18972243 /* global angularModule: true,
18982244 version: true,
18992245
19002246 $LocaleProvider,
19012247 $CompileProvider,
19022248
1903 htmlAnchorDirective,
1904 inputDirective,
1905 inputDirective,
1906 formDirective,
1907 scriptDirective,
1908 selectDirective,
1909 styleDirective,
1910 optionDirective,
1911 ngBindDirective,
1912 ngBindHtmlDirective,
1913 ngBindTemplateDirective,
1914 ngClassDirective,
1915 ngClassEvenDirective,
1916 ngClassOddDirective,
1917 ngCspDirective,
1918 ngCloakDirective,
1919 ngControllerDirective,
1920 ngFormDirective,
1921 ngHideDirective,
1922 ngIfDirective,
1923 ngIncludeDirective,
1924 ngIncludeFillContentDirective,
1925 ngInitDirective,
1926 ngNonBindableDirective,
1927 ngPluralizeDirective,
1928 ngRepeatDirective,
1929 ngShowDirective,
1930 ngStyleDirective,
1931 ngSwitchDirective,
1932 ngSwitchWhenDirective,
1933 ngSwitchDefaultDirective,
1934 ngOptionsDirective,
1935 ngTranscludeDirective,
1936 ngModelDirective,
1937 ngListDirective,
1938 ngChangeDirective,
1939 requiredDirective,
1940 requiredDirective,
1941 ngValueDirective,
1942 ngAttributeAliasDirectives,
1943 ngEventDirectives,
1944
1945 $AnchorScrollProvider,
1946 $AnimateProvider,
1947 $BrowserProvider,
1948 $CacheFactoryProvider,
1949 $ControllerProvider,
1950 $DocumentProvider,
1951 $ExceptionHandlerProvider,
1952 $FilterProvider,
1953 $InterpolateProvider,
1954 $IntervalProvider,
1955 $HttpProvider,
1956 $HttpBackendProvider,
1957 $LocationProvider,
1958 $LogProvider,
1959 $ParseProvider,
1960 $RootScopeProvider,
1961 $QProvider,
1962 $$SanitizeUriProvider,
1963 $SceProvider,
1964 $SceDelegateProvider,
1965 $SnifferProvider,
1966 $TemplateCacheProvider,
1967 $TimeoutProvider,
1968 $$RAFProvider,
1969 $$AsyncCallbackProvider,
1970 $WindowProvider
2249 htmlAnchorDirective,
2250 inputDirective,
2251 inputDirective,
2252 formDirective,
2253 scriptDirective,
2254 selectDirective,
2255 styleDirective,
2256 optionDirective,
2257 ngBindDirective,
2258 ngBindHtmlDirective,
2259 ngBindTemplateDirective,
2260 ngClassDirective,
2261 ngClassEvenDirective,
2262 ngClassOddDirective,
2263 ngCspDirective,
2264 ngCloakDirective,
2265 ngControllerDirective,
2266 ngFormDirective,
2267 ngHideDirective,
2268 ngIfDirective,
2269 ngIncludeDirective,
2270 ngIncludeFillContentDirective,
2271 ngInitDirective,
2272 ngNonBindableDirective,
2273 ngPluralizeDirective,
2274 ngRepeatDirective,
2275 ngShowDirective,
2276 ngStyleDirective,
2277 ngSwitchDirective,
2278 ngSwitchWhenDirective,
2279 ngSwitchDefaultDirective,
2280 ngOptionsDirective,
2281 ngTranscludeDirective,
2282 ngModelDirective,
2283 ngListDirective,
2284 ngChangeDirective,
2285 patternDirective,
2286 patternDirective,
2287 requiredDirective,
2288 requiredDirective,
2289 minlengthDirective,
2290 minlengthDirective,
2291 maxlengthDirective,
2292 maxlengthDirective,
2293 ngValueDirective,
2294 ngModelOptionsDirective,
2295 ngAttributeAliasDirectives,
2296 ngEventDirectives,
2297
2298 $AnchorScrollProvider,
2299 $AnimateProvider,
2300 $$CoreAnimateQueueProvider,
2301 $$CoreAnimateRunnerProvider,
2302 $BrowserProvider,
2303 $CacheFactoryProvider,
2304 $ControllerProvider,
2305 $DocumentProvider,
2306 $ExceptionHandlerProvider,
2307 $FilterProvider,
2308 $InterpolateProvider,
2309 $IntervalProvider,
2310 $$HashMapProvider,
2311 $HttpProvider,
2312 $HttpParamSerializerProvider,
2313 $HttpParamSerializerJQLikeProvider,
2314 $HttpBackendProvider,
2315 $LocationProvider,
2316 $LogProvider,
2317 $ParseProvider,
2318 $RootScopeProvider,
2319 $QProvider,
2320 $$QProvider,
2321 $$SanitizeUriProvider,
2322 $SceProvider,
2323 $SceDelegateProvider,
2324 $SnifferProvider,
2325 $TemplateCacheProvider,
2326 $TemplateRequestProvider,
2327 $$TestabilityProvider,
2328 $TimeoutProvider,
2329 $$RAFProvider,
2330 $WindowProvider,
2331 $$jqLiteProvider,
2332 $$CookieReaderProvider
19712333 */
19722334
19732335
19862348 * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
19872349 */
19882350 var version = {
1989 full: '1.2.23', // all of these placeholder strings will be replaced by grunt's
2351 full: '1.4.3', // all of these placeholder strings will be replaced by grunt's
19902352 major: 1, // package task
1991 minor: 2,
1992 dot: 23,
1993 codeName: 'superficial-malady'
2353 minor: 4,
2354 dot: 3,
2355 codeName: 'foam-acceleration'
19942356 };
19952357
19962358
1997 function publishExternalAPI(angular){
2359 function publishExternalAPI(angular) {
19982360 extend(angular, {
19992361 'bootstrap': bootstrap,
20002362 'copy': copy,
20012363 'extend': extend,
2364 'merge': merge,
20022365 'equals': equals,
20032366 'element': jqLite,
20042367 'forEach': forEach,
20212384 'lowercase': lowercase,
20222385 'uppercase': uppercase,
20232386 'callbacks': {counter: 0},
2387 'getTestability': getTestability,
20242388 '$$minErr': minErr,
2025 '$$csp': csp
2389 '$$csp': csp,
2390 'reloadWithDebugInfo': reloadWithDebugInfo
20262391 });
20272392
20282393 angularModule = setupModuleLoader(window);
20742439 ngModel: ngModelDirective,
20752440 ngList: ngListDirective,
20762441 ngChange: ngChangeDirective,
2442 pattern: patternDirective,
2443 ngPattern: patternDirective,
20772444 required: requiredDirective,
20782445 ngRequired: requiredDirective,
2079 ngValue: ngValueDirective
2446 minlength: minlengthDirective,
2447 ngMinlength: minlengthDirective,
2448 maxlength: maxlengthDirective,
2449 ngMaxlength: maxlengthDirective,
2450 ngValue: ngValueDirective,
2451 ngModelOptions: ngModelOptionsDirective
20802452 }).
20812453 directive({
20822454 ngInclude: ngIncludeFillContentDirective
20862458 $provide.provider({
20872459 $anchorScroll: $AnchorScrollProvider,
20882460 $animate: $AnimateProvider,
2461 $$animateQueue: $$CoreAnimateQueueProvider,
2462 $$AnimateRunner: $$CoreAnimateRunnerProvider,
20892463 $browser: $BrowserProvider,
20902464 $cacheFactory: $CacheFactoryProvider,
20912465 $controller: $ControllerProvider,
20952469 $interpolate: $InterpolateProvider,
20962470 $interval: $IntervalProvider,
20972471 $http: $HttpProvider,
2472 $httpParamSerializer: $HttpParamSerializerProvider,
2473 $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
20982474 $httpBackend: $HttpBackendProvider,
20992475 $location: $LocationProvider,
21002476 $log: $LogProvider,
21012477 $parse: $ParseProvider,
21022478 $rootScope: $RootScopeProvider,
21032479 $q: $QProvider,
2480 $$q: $$QProvider,
21042481 $sce: $SceProvider,
21052482 $sceDelegate: $SceDelegateProvider,
21062483 $sniffer: $SnifferProvider,
21072484 $templateCache: $TemplateCacheProvider,
2485 $templateRequest: $TemplateRequestProvider,
2486 $$testability: $$TestabilityProvider,
21082487 $timeout: $TimeoutProvider,
21092488 $window: $WindowProvider,
21102489 $$rAF: $$RAFProvider,
2111 $$asyncCallback : $$AsyncCallbackProvider
2490 $$jqLite: $$jqLiteProvider,
2491 $$HashMap: $$HashMapProvider,
2492 $$cookieReader: $$CookieReaderProvider
21122493 });
21132494 }
21142495 ]);
21152496 }
2497
2498 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2499 * Any commits to this file should be reviewed with security in mind. *
2500 * Changes to this file can potentially create security vulnerabilities. *
2501 * An approval from 2 Core members with history of modifying *
2502 * this file is required. *
2503 * *
2504 * Does the change somehow allow for arbitrary javascript to be executed? *
2505 * Or allows for someone to change the prototype of built-in objects? *
2506 * Or gives undesired access to variables likes document or window? *
2507 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
21162508
21172509 /* global JQLitePrototype: true,
21182510 addEventListenerFn: true,
21192511 removeEventListenerFn: true,
2120 BOOLEAN_ATTR: true
2512 BOOLEAN_ATTR: true,
2513 ALIASED_ATTR: true,
21212514 */
21222515
21232516 //////////////////////////////////
21412534 * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most
21422535 * commonly needed functionality with the goal of having a very small footprint.</div>
21432536 *
2144 * To use jQuery, simply load it before `DOMContentLoaded` event fired.
2537 * To use `jQuery`, simply ensure it is loaded before the `angular.js` file.
21452538 *
21462539 * <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or
21472540 * jqLite; they are never raw DOM references.</div>
21522545 * - [`addClass()`](http://api.jquery.com/addClass/)
21532546 * - [`after()`](http://api.jquery.com/after/)
21542547 * - [`append()`](http://api.jquery.com/append/)
2155 * - [`attr()`](http://api.jquery.com/attr/)
2548 * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters
21562549 * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
21572550 * - [`children()`](http://api.jquery.com/children/) - Does not support selectors
21582551 * - [`clone()`](http://api.jquery.com/clone/)
21592552 * - [`contents()`](http://api.jquery.com/contents/)
2160 * - [`css()`](http://api.jquery.com/css/)
2553 * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'.
21612554 * - [`data()`](http://api.jquery.com/data/)
2555 * - [`detach()`](http://api.jquery.com/detach/)
21622556 * - [`empty()`](http://api.jquery.com/empty/)
21632557 * - [`eq()`](http://api.jquery.com/eq/)
21642558 * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name
21992593 * `'ngModel'`).
22002594 * - `injector()` - retrieves the injector of the current element or its parent.
22012595 * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current
2202 * element or its parent.
2596 * element or its parent. Requires {@link guide/production#disabling-debug-data Debug Data} to
2597 * be enabled.
22032598 * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the
22042599 * current element. This getter should be used only on elements that contain a directive which starts a new isolate
22052600 * scope. Calling `scope()` on this element always returns the original non-isolate scope.
2601 * Requires {@link guide/production#disabling-debug-data Debug Data} to be enabled.
22062602 * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top
22072603 * parent element is reached.
22082604 *
22142610
22152611 var jqCache = JQLite.cache = {},
22162612 jqId = 1,
2217 addEventListenerFn = (window.document.addEventListener
2218 ? function(element, type, fn) {element.addEventListener(type, fn, false);}
2219 : function(element, type, fn) {element.attachEvent('on' + type, fn);}),
2220 removeEventListenerFn = (window.document.removeEventListener
2221 ? function(element, type, fn) {element.removeEventListener(type, fn, false); }
2222 : function(element, type, fn) {element.detachEvent('on' + type, fn); });
2613 addEventListenerFn = function(element, type, fn) {
2614 element.addEventListener(type, fn, false);
2615 },
2616 removeEventListenerFn = function(element, type, fn) {
2617 element.removeEventListener(type, fn, false);
2618 };
22232619
22242620 /*
22252621 * !!! This is an undocumented "private" function !!!
22262622 */
2227 var jqData = JQLite._data = function(node) {
2623 JQLite._data = function(node) {
22282624 //jQuery always returns an object on cache miss
22292625 return this.cache[node[this.expando]] || {};
22302626 };
22342630
22352631 var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
22362632 var MOZ_HACK_REGEXP = /^moz([A-Z])/;
2633 var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"};
22372634 var jqLiteMinErr = minErr('jqLite');
22382635
22392636 /**
22492646 replace(MOZ_HACK_REGEXP, 'Moz$1');
22502647 }
22512648
2252 /////////////////////////////////////////////
2253 // jQuery mutation patch
2254 //
2255 // In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a
2256 // $destroy event on all DOM nodes being removed.
2257 //
2258 /////////////////////////////////////////////
2259
2260 function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) {
2261 var originalJqFn = jQuery.fn[name];
2262 originalJqFn = originalJqFn.$original || originalJqFn;
2263 removePatch.$original = originalJqFn;
2264 jQuery.fn[name] = removePatch;
2265
2266 function removePatch(param) {
2267 // jshint -W040
2268 var list = filterElems && param ? [this.filter(param)] : [this],
2269 fireEvent = dispatchThis,
2270 set, setIndex, setLength,
2271 element, childIndex, childLength, children;
2272
2273 if (!getterIfNoArguments || param != null) {
2274 while(list.length) {
2275 set = list.shift();
2276 for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) {
2277 element = jqLite(set[setIndex]);
2278 if (fireEvent) {
2279 element.triggerHandler('$destroy');
2280 } else {
2281 fireEvent = !fireEvent;
2282 }
2283 for(childIndex = 0, childLength = (children = element.children()).length;
2284 childIndex < childLength;
2285 childIndex++) {
2286 list.push(jQuery(children[childIndex]));
2287 }
2288 }
2289 }
2290 }
2291 return originalJqFn.apply(this, arguments);
2292 }
2293 }
2294
22952649 var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/;
22962650 var HTML_REGEXP = /<|&#?\w+;/;
22972651 var TAG_NAME_REGEXP = /<([\w:]+)/;
23112665 wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
23122666 wrapMap.th = wrapMap.td;
23132667
2668
23142669 function jqLiteIsTextNode(html) {
23152670 return !HTML_REGEXP.test(html);
23162671 }
23172672
2673 function jqLiteAcceptsData(node) {
2674 // The window object can accept data but has no nodeType
2675 // Otherwise we are only interested in elements (1) and documents (9)
2676 var nodeType = node.nodeType;
2677 return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT;
2678 }
2679
2680 function jqLiteHasData(node) {
2681 for (var key in jqCache[node.ng339]) {
2682 return true;
2683 }
2684 return false;
2685 }
2686
23182687 function jqLiteBuildFragment(html, context) {
2319 var elem, tmp, tag, wrap,
2688 var tmp, tag, wrap,
23202689 fragment = context.createDocumentFragment(),
2321 nodes = [], i, j, jj;
2690 nodes = [], i;
23222691
23232692 if (jqLiteIsTextNode(html)) {
23242693 // Convert non-html into a text node
23252694 nodes.push(context.createTextNode(html));
23262695 } else {
2327 tmp = fragment.appendChild(context.createElement('div'));
23282696 // Convert html into DOM nodes
2697 tmp = tmp || fragment.appendChild(context.createElement("div"));
23292698 tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase();
23302699 wrap = wrapMap[tag] || wrapMap._default;
2331 tmp.innerHTML = '<div>&#160;</div>' +
2332 wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2];
2333 tmp.removeChild(tmp.firstChild);
2700 tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2];
23342701
23352702 // Descend through wrappers to the right content
23362703 i = wrap[0];
23382705 tmp = tmp.lastChild;
23392706 }
23402707
2341 for (j=0, jj=tmp.childNodes.length; j<jj; ++j) nodes.push(tmp.childNodes[j]);
2708 nodes = concat(nodes, tmp.childNodes);
23422709
23432710 tmp = fragment.firstChild;
23442711 tmp.textContent = "";
23472714 // Remove wrapper from fragment
23482715 fragment.textContent = "";
23492716 fragment.innerHTML = ""; // Clear inner HTML
2350 return nodes;
2717 forEach(nodes, function(node) {
2718 fragment.appendChild(node);
2719 });
2720
2721 return fragment;
23512722 }
23522723
23532724 function jqLiteParseHTML(html, context) {
23582729 return [context.createElement(parsed[1])];
23592730 }
23602731
2361 return jqLiteBuildFragment(html, context);
2732 if ((parsed = jqLiteBuildFragment(html, context))) {
2733 return parsed.childNodes;
2734 }
2735
2736 return [];
23622737 }
23632738
23642739 /////////////////////////////////////////////
23662741 if (element instanceof JQLite) {
23672742 return element;
23682743 }
2744
2745 var argIsString;
2746
23692747 if (isString(element)) {
23702748 element = trim(element);
2749 argIsString = true;
23712750 }
23722751 if (!(this instanceof JQLite)) {
2373 if (isString(element) && element.charAt(0) != '<') {
2752 if (argIsString && element.charAt(0) != '<') {
23742753 throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element');
23752754 }
23762755 return new JQLite(element);
23772756 }
23782757
2379 if (isString(element)) {
2758 if (argIsString) {
23802759 jqLiteAddNodes(this, jqLiteParseHTML(element));
2381 var fragment = jqLite(document.createDocumentFragment());
2382 fragment.append(this);
23832760 } else {
23842761 jqLiteAddNodes(this, element);
23852762 }
23892766 return element.cloneNode(true);
23902767 }
23912768
2392 function jqLiteDealoc(element){
2393 jqLiteRemoveData(element);
2394 for ( var i = 0, children = element.childNodes || []; i < children.length; i++) {
2395 jqLiteDealoc(children[i]);
2769 function jqLiteDealoc(element, onlyDescendants) {
2770 if (!onlyDescendants) jqLiteRemoveData(element);
2771
2772 if (element.querySelectorAll) {
2773 var descendants = element.querySelectorAll('*');
2774 for (var i = 0, l = descendants.length; i < l; i++) {
2775 jqLiteRemoveData(descendants[i]);
2776 }
23962777 }
23972778 }
23982779
23992780 function jqLiteOff(element, type, fn, unsupported) {
24002781 if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument');
24012782
2402 var events = jqLiteExpandoStore(element, 'events'),
2403 handle = jqLiteExpandoStore(element, 'handle');
2783 var expandoStore = jqLiteExpandoStore(element);
2784 var events = expandoStore && expandoStore.events;
2785 var handle = expandoStore && expandoStore.handle;
24042786
24052787 if (!handle) return; //no listeners registered
24062788
2407 if (isUndefined(type)) {
2408 forEach(events, function(eventHandler, type) {
2409 removeEventListenerFn(element, type, eventHandler);
2789 if (!type) {
2790 for (type in events) {
2791 if (type !== '$destroy') {
2792 removeEventListenerFn(element, type, handle);
2793 }
24102794 delete events[type];
2411 });
2795 }
24122796 } else {
24132797 forEach(type.split(' '), function(type) {
2414 if (isUndefined(fn)) {
2415 removeEventListenerFn(element, type, events[type]);
2416 delete events[type];
2417 } else {
2418 arrayRemove(events[type] || [], fn);
2419 }
2798 if (isDefined(fn)) {
2799 var listenerFns = events[type];
2800 arrayRemove(listenerFns || [], fn);
2801 if (listenerFns && listenerFns.length > 0) {
2802 return;
2803 }
2804 }
2805
2806 removeEventListenerFn(element, type, handle);
2807 delete events[type];
24202808 });
24212809 }
24222810 }
24232811
24242812 function jqLiteRemoveData(element, name) {
2425 var expandoId = element.ng339,
2426 expandoStore = jqCache[expandoId];
2813 var expandoId = element.ng339;
2814 var expandoStore = expandoId && jqCache[expandoId];
24272815
24282816 if (expandoStore) {
24292817 if (name) {
2430 delete jqCache[expandoId].data[name];
2818 delete expandoStore.data[name];
24312819 return;
24322820 }
24332821
24342822 if (expandoStore.handle) {
2435 expandoStore.events.$destroy && expandoStore.handle({}, '$destroy');
2823 if (expandoStore.events.$destroy) {
2824 expandoStore.handle({}, '$destroy');
2825 }
24362826 jqLiteOff(element);
24372827 }
24382828 delete jqCache[expandoId];
24402830 }
24412831 }
24422832
2443 function jqLiteExpandoStore(element, key, value) {
2833
2834 function jqLiteExpandoStore(element, createIfNecessary) {
24442835 var expandoId = element.ng339,
2445 expandoStore = jqCache[expandoId || -1];
2446
2447 if (isDefined(value)) {
2448 if (!expandoStore) {
2449 element.ng339 = expandoId = jqNextId();
2450 expandoStore = jqCache[expandoId] = {};
2451 }
2452 expandoStore[key] = value;
2453 } else {
2454 return expandoStore && expandoStore[key];
2836 expandoStore = expandoId && jqCache[expandoId];
2837
2838 if (createIfNecessary && !expandoStore) {
2839 element.ng339 = expandoId = jqNextId();
2840 expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined};
24552841 }
2842
2843 return expandoStore;
24562844 }
24572845
2846
24582847 function jqLiteData(element, key, value) {
2459 var data = jqLiteExpandoStore(element, 'data'),
2460 isSetter = isDefined(value),
2461 keyDefined = !isSetter && isDefined(key),
2462 isSimpleGetter = keyDefined && !isObject(key);
2463
2464 if (!data && !isSimpleGetter) {
2465 jqLiteExpandoStore(element, 'data', data = {});
2466 }
2467
2468 if (isSetter) {
2469 data[key] = value;
2470 } else {
2471 if (keyDefined) {
2472 if (isSimpleGetter) {
2473 // don't create data in this case.
2474 return data && data[key];
2848 if (jqLiteAcceptsData(element)) {
2849
2850 var isSimpleSetter = isDefined(value);
2851 var isSimpleGetter = !isSimpleSetter && key && !isObject(key);
2852 var massGetter = !key;
2853 var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter);
2854 var data = expandoStore && expandoStore.data;
2855
2856 if (isSimpleSetter) { // data('key', value)
2857 data[key] = value;
2858 } else {
2859 if (massGetter) { // data()
2860 return data;
24752861 } else {
2476 extend(data, key);
2477 }
2478 } else {
2479 return data;
2862 if (isSimpleGetter) { // data('key')
2863 // don't force creation of expandoStore if it doesn't exist yet
2864 return data && data[key];
2865 } else { // mass-setter: data({key1: val1, key2: val2})
2866 extend(data, key);
2867 }
2868 }
24802869 }
24812870 }
24822871 }
24842873 function jqLiteHasClass(element, selector) {
24852874 if (!element.getAttribute) return false;
24862875 return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " ").
2487 indexOf( " " + selector + " " ) > -1);
2876 indexOf(" " + selector + " ") > -1);
24882877 }
24892878
24902879 function jqLiteRemoveClass(element, cssClasses) {
25152904 }
25162905 }
25172906
2907
25182908 function jqLiteAddNodes(root, elements) {
2909 // THIS CODE IS VERY HOT. Don't make changes without benchmarking.
2910
25192911 if (elements) {
2520 elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements))
2521 ? elements
2522 : [ elements ];
2523 for(var i=0; i < elements.length; i++) {
2524 root.push(elements[i]);
2912
2913 // if a Node (the most common case)
2914 if (elements.nodeType) {
2915 root[root.length++] = elements;
2916 } else {
2917 var length = elements.length;
2918
2919 // if an Array or NodeList and not a Window
2920 if (typeof length === 'number' && elements.window !== elements) {
2921 if (length) {
2922 for (var i = 0; i < length; i++) {
2923 root[root.length++] = elements[i];
2924 }
2925 }
2926 } else {
2927 root[root.length++] = elements;
2928 }
25252929 }
25262930 }
25272931 }
25282932
2933
25292934 function jqLiteController(element, name) {
2530 return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller');
2935 return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller');
25312936 }
25322937
25332938 function jqLiteInheritedData(element, name, value) {
25342939 // if element is the document object work with the html element instead
25352940 // this makes $(document).scope() possible
2536 if(element.nodeType == 9) {
2941 if (element.nodeType == NODE_TYPE_DOCUMENT) {
25372942 element = element.documentElement;
25382943 }
25392944 var names = isArray(name) ? name : [name];
25462951 // If dealing with a document fragment node with a host element, and no parent, use the host
25472952 // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM
25482953 // to lookup parent controllers.
2549 element = element.parentNode || (element.nodeType === 11 && element.host);
2954 element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host);
25502955 }
25512956 }
25522957
25532958 function jqLiteEmpty(element) {
2554 for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) {
2555 jqLiteDealoc(childNodes[i]);
2556 }
2959 jqLiteDealoc(element, true);
25572960 while (element.firstChild) {
25582961 element.removeChild(element.firstChild);
2962 }
2963 }
2964
2965 function jqLiteRemove(element, keepData) {
2966 if (!keepData) jqLiteDealoc(element);
2967 var parent = element.parentNode;
2968 if (parent) parent.removeChild(element);
2969 }
2970
2971
2972 function jqLiteDocumentLoaded(action, win) {
2973 win = win || window;
2974 if (win.document.readyState === 'complete') {
2975 // Force the action to be run async for consistent behaviour
2976 // from the action's point of view
2977 // i.e. it will definitely not be in a $apply
2978 win.setTimeout(action);
2979 } else {
2980 // No need to unbind this handler as load is only ever called once
2981 jqLite(win).on('load', action);
25592982 }
25602983 }
25612984
25722995 fn();
25732996 }
25742997
2575 // check if document already is loaded
2576 if (document.readyState === 'complete'){
2998 // check if document is already loaded
2999 if (document.readyState === 'complete') {
25773000 setTimeout(trigger);
25783001 } else {
25793002 this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9
25853008 },
25863009 toString: function() {
25873010 var value = [];
2588 forEach(this, function(e){ value.push('' + e);});
3011 forEach(this, function(e) { value.push('' + e);});
25893012 return '[' + value.join(', ') + ']';
25903013 },
25913014
26103033 });
26113034 var BOOLEAN_ELEMENTS = {};
26123035 forEach('input,select,option,textarea,button,form,details'.split(','), function(value) {
2613 BOOLEAN_ELEMENTS[uppercase(value)] = true;
3036 BOOLEAN_ELEMENTS[value] = true;
26143037 });
3038 var ALIASED_ATTR = {
3039 'ngMinlength': 'minlength',
3040 'ngMaxlength': 'maxlength',
3041 'ngMin': 'min',
3042 'ngMax': 'max',
3043 'ngPattern': 'pattern'
3044 };
26153045
26163046 function getBooleanAttrName(element, name) {
26173047 // check dom last since we will most likely fail on name
26183048 var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()];
26193049
26203050 // booleanAttr is here twice to minimize DOM access
2621 return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr;
3051 return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr;
3052 }
3053
3054 function getAliasedAttrName(element, name) {
3055 var nodeName = element.nodeName;
3056 return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name];
26223057 }
26233058
26243059 forEach({
26253060 data: jqLiteData,
2626 removeData: jqLiteRemoveData
3061 removeData: jqLiteRemoveData,
3062 hasData: jqLiteHasData
26273063 }, function(fn, name) {
26283064 JQLite[name] = fn;
26293065 });
26483084 return jqLiteInheritedData(element, '$injector');
26493085 },
26503086
2651 removeAttr: function(element,name) {
3087 removeAttr: function(element, name) {
26523088 element.removeAttribute(name);
26533089 },
26543090
26603096 if (isDefined(value)) {
26613097 element.style[name] = value;
26623098 } else {
2663 var val;
2664
2665 if (msie <= 8) {
2666 // this is some IE specific weirdness that jQuery 1.6.4 does not sure why
2667 val = element.currentStyle && element.currentStyle[name];
2668 if (val === '') val = 'auto';
2669 }
2670
2671 val = val || element.style[name];
2672
2673 if (msie <= 8) {
2674 // jquery weirdness :-/
2675 val = (val === '') ? undefined : val;
2676 }
2677
2678 return val;
3099 return element.style[name];
26793100 }
26803101 },
26813102
2682 attr: function(element, name, value){
3103 attr: function(element, name, value) {
3104 var nodeType = element.nodeType;
3105 if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) {
3106 return;
3107 }
26833108 var lowercasedName = lowercase(name);
26843109 if (BOOLEAN_ATTR[lowercasedName]) {
26853110 if (isDefined(value)) {
26923117 }
26933118 } else {
26943119 return (element[name] ||
2695 (element.attributes.getNamedItem(name)|| noop).specified)
3120 (element.attributes.getNamedItem(name) || noop).specified)
26963121 ? lowercasedName
26973122 : undefined;
26983123 }
27163141 },
27173142
27183143 text: (function() {
2719 var NODE_TYPE_TEXT_PROPERTY = [];
2720 if (msie < 9) {
2721 NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/
2722 NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/
2723 } else {
2724 NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/
2725 NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/
2726 }
27273144 getText.$dv = '';
27283145 return getText;
27293146
27303147 function getText(element, value) {
2731 var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType];
27323148 if (isUndefined(value)) {
2733 return textProp ? element[textProp] : '';
2734 }
2735 element[textProp] = value;
3149 var nodeType = element.nodeType;
3150 return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : '';
3151 }
3152 element.textContent = value;
27363153 }
27373154 })(),
27383155
27393156 val: function(element, value) {
27403157 if (isUndefined(value)) {
2741 if (nodeName_(element) === 'SELECT' && element.multiple) {
3158 if (element.multiple && nodeName_(element) === 'select') {
27423159 var result = [];
2743 forEach(element.options, function (option) {
3160 forEach(element.options, function(option) {
27443161 if (option.selected) {
27453162 result.push(option.value || option.text);
27463163 }
27563173 if (isUndefined(value)) {
27573174 return element.innerHTML;
27583175 }
2759 for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) {
2760 jqLiteDealoc(childNodes[i]);
2761 }
3176 jqLiteDealoc(element, true);
27623177 element.innerHTML = value;
27633178 },
27643179
27653180 empty: jqLiteEmpty
2766 }, function(fn, name){
3181 }, function(fn, name) {
27673182 /**
27683183 * Properties: writes return selection, reads return first value
27693184 */
28153230 });
28163231
28173232 function createEventHandler(element, events) {
2818 var eventHandler = function (event, type) {
2819 if (!event.preventDefault) {
2820 event.preventDefault = function() {
2821 event.returnValue = false; //ie
3233 var eventHandler = function(event, type) {
3234 // jQuery specific api
3235 event.isDefaultPrevented = function() {
3236 return event.defaultPrevented;
3237 };
3238
3239 var eventFns = events[type || event.type];
3240 var eventFnsLength = eventFns ? eventFns.length : 0;
3241
3242 if (!eventFnsLength) return;
3243
3244 if (isUndefined(event.immediatePropagationStopped)) {
3245 var originalStopImmediatePropagation = event.stopImmediatePropagation;
3246 event.stopImmediatePropagation = function() {
3247 event.immediatePropagationStopped = true;
3248
3249 if (event.stopPropagation) {
3250 event.stopPropagation();
3251 }
3252
3253 if (originalStopImmediatePropagation) {
3254 originalStopImmediatePropagation.call(event);
3255 }
28223256 };
28233257 }
28243258
2825 if (!event.stopPropagation) {
2826 event.stopPropagation = function() {
2827 event.cancelBubble = true; //ie
2828 };
2829 }
2830
2831 if (!event.target) {
2832 event.target = event.srcElement || document;
2833 }
2834
2835 if (isUndefined(event.defaultPrevented)) {
2836 var prevent = event.preventDefault;
2837 event.preventDefault = function() {
2838 event.defaultPrevented = true;
2839 prevent.call(event);
2840 };
2841 event.defaultPrevented = false;
2842 }
2843
2844 event.isDefaultPrevented = function() {
2845 return event.defaultPrevented || event.returnValue === false;
3259 event.isImmediatePropagationStopped = function() {
3260 return event.immediatePropagationStopped === true;
28463261 };
28473262
28483263 // Copy event handlers in case event handlers array is modified during execution.
2849 var eventHandlersCopy = shallowCopy(events[type || event.type] || []);
2850
2851 forEach(eventHandlersCopy, function(fn) {
2852 fn.call(element, event);
2853 });
2854
2855 // Remove monkey-patched methods (IE),
2856 // as they would cause memory leaks in IE8.
2857 if (msie <= 8) {
2858 // IE7/8 does not allow to delete property on native object
2859 event.preventDefault = null;
2860 event.stopPropagation = null;
2861 event.isDefaultPrevented = null;
2862 } else {
2863 // It shouldn't affect normal browsers (native methods are defined on prototype).
2864 delete event.preventDefault;
2865 delete event.stopPropagation;
2866 delete event.isDefaultPrevented;
3264 if ((eventFnsLength > 1)) {
3265 eventFns = shallowCopy(eventFns);
3266 }
3267
3268 for (var i = 0; i < eventFnsLength; i++) {
3269 if (!event.isImmediatePropagationStopped()) {
3270 eventFns[i].call(element, event);
3271 }
28673272 }
28683273 };
3274
3275 // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all
3276 // events on `element`
28693277 eventHandler.elem = element;
28703278 return eventHandler;
28713279 }
28783286 forEach({
28793287 removeData: jqLiteRemoveData,
28803288
2881 dealoc: jqLiteDealoc,
2882
2883 on: function onFn(element, type, fn, unsupported){
3289 on: function jqLiteOn(element, type, fn, unsupported) {
28843290 if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters');
28853291
2886 var events = jqLiteExpandoStore(element, 'events'),
2887 handle = jqLiteExpandoStore(element, 'handle');
2888
2889 if (!events) jqLiteExpandoStore(element, 'events', events = {});
2890 if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events));
2891
2892 forEach(type.split(' '), function(type){
3292 // Do not add event handlers to non-elements because they will not be cleaned up.
3293 if (!jqLiteAcceptsData(element)) {
3294 return;
3295 }
3296
3297 var expandoStore = jqLiteExpandoStore(element, true);
3298 var events = expandoStore.events;
3299 var handle = expandoStore.handle;
3300
3301 if (!handle) {
3302 handle = expandoStore.handle = createEventHandler(element, events);
3303 }
3304
3305 // http://jsperf.com/string-indexof-vs-split
3306 var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type];
3307 var i = types.length;
3308
3309 while (i--) {
3310 type = types[i];
28933311 var eventFns = events[type];
28943312
28953313 if (!eventFns) {
2896 if (type == 'mouseenter' || type == 'mouseleave') {
2897 var contains = document.body.contains || document.body.compareDocumentPosition ?
2898 function( a, b ) {
2899 // jshint bitwise: false
2900 var adown = a.nodeType === 9 ? a.documentElement : a,
2901 bup = b && b.parentNode;
2902 return a === bup || !!( bup && bup.nodeType === 1 && (
2903 adown.contains ?
2904 adown.contains( bup ) :
2905 a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
2906 ));
2907 } :
2908 function( a, b ) {
2909 if ( b ) {
2910 while ( (b = b.parentNode) ) {
2911 if ( b === a ) {
2912 return true;
2913 }
2914 }
2915 }
2916 return false;
2917 };
2918
2919 events[type] = [];
2920
3314 events[type] = [];
3315
3316 if (type === 'mouseenter' || type === 'mouseleave') {
29213317 // Refer to jQuery's implementation of mouseenter & mouseleave
29223318 // Read about mouseenter and mouseleave:
29233319 // http://www.quirksmode.org/js/events_mouse.html#link8
2924 var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"};
2925
2926 onFn(element, eventmap[type], function(event) {
3320
3321 jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) {
29273322 var target = this, related = event.relatedTarget;
29283323 // For mousenter/leave call the handler if related is outside the target.
29293324 // NB: No relatedTarget if the mouse left/entered the browser window
2930 if ( !related || (related !== target && !contains(target, related)) ){
3325 if (!related || (related !== target && !target.contains(related))) {
29313326 handle(event, type);
29323327 }
29333328 });
29343329
29353330 } else {
2936 addEventListenerFn(element, type, handle);
2937 events[type] = [];
3331 if (type !== '$destroy') {
3332 addEventListenerFn(element, type, handle);
3333 }
29383334 }
29393335 eventFns = events[type];
29403336 }
29413337 eventFns.push(fn);
2942 });
3338 }
29433339 },
29443340
29453341 off: jqLiteOff,
29603356 replaceWith: function(element, replaceNode) {
29613357 var index, parent = element.parentNode;
29623358 jqLiteDealoc(element);
2963 forEach(new JQLite(replaceNode), function(node){
3359 forEach(new JQLite(replaceNode), function(node) {
29643360 if (index) {
29653361 parent.insertBefore(node, index.nextSibling);
29663362 } else {
29723368
29733369 children: function(element) {
29743370 var children = [];
2975 forEach(element.childNodes, function(element){
2976 if (element.nodeType === 1)
3371 forEach(element.childNodes, function(element) {
3372 if (element.nodeType === NODE_TYPE_ELEMENT) {
29773373 children.push(element);
3374 }
29783375 });
29793376 return children;
29803377 },
29843381 },
29853382
29863383 append: function(element, node) {
2987 forEach(new JQLite(node), function(child){
2988 if (element.nodeType === 1 || element.nodeType === 11) {
2989 element.appendChild(child);
2990 }
2991 });
3384 var nodeType = element.nodeType;
3385 if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return;
3386
3387 node = new JQLite(node);
3388
3389 for (var i = 0, ii = node.length; i < ii; i++) {
3390 var child = node[i];
3391 element.appendChild(child);
3392 }
29923393 },
29933394
29943395 prepend: function(element, node) {
2995 if (element.nodeType === 1) {
3396 if (element.nodeType === NODE_TYPE_ELEMENT) {
29963397 var index = element.firstChild;
2997 forEach(new JQLite(node), function(child){
3398 forEach(new JQLite(node), function(child) {
29983399 element.insertBefore(child, index);
29993400 });
30003401 }
30013402 },
30023403
30033404 wrap: function(element, wrapNode) {
3004 wrapNode = jqLite(wrapNode)[0];
3405 wrapNode = jqLite(wrapNode).eq(0).clone()[0];
30053406 var parent = element.parentNode;
30063407 if (parent) {
30073408 parent.replaceChild(wrapNode, element);
30093410 wrapNode.appendChild(element);
30103411 },
30113412
3012 remove: function(element) {
3013 jqLiteDealoc(element);
3014 var parent = element.parentNode;
3015 if (parent) parent.removeChild(element);
3413 remove: jqLiteRemove,
3414
3415 detach: function(element) {
3416 jqLiteRemove(element, true);
30163417 },
30173418
30183419 after: function(element, newElement) {
30193420 var index = element, parent = element.parentNode;
3020 forEach(new JQLite(newElement), function(node){
3421 newElement = new JQLite(newElement);
3422
3423 for (var i = 0, ii = newElement.length; i < ii; i++) {
3424 var node = newElement[i];
30213425 parent.insertBefore(node, index.nextSibling);
30223426 index = node;
3023 });
3427 }
30243428 },
30253429
30263430 addClass: jqLiteAddClass,
30283432
30293433 toggleClass: function(element, selector, condition) {
30303434 if (selector) {
3031 forEach(selector.split(' '), function(className){
3435 forEach(selector.split(' '), function(className) {
30323436 var classCondition = condition;
30333437 if (isUndefined(classCondition)) {
30343438 classCondition = !jqLiteHasClass(element, className);
30403444
30413445 parent: function(element) {
30423446 var parent = element.parentNode;
3043 return parent && parent.nodeType !== 11 ? parent : null;
3447 return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null;
30443448 },
30453449
30463450 next: function(element) {
3047 if (element.nextElementSibling) {
3048 return element.nextElementSibling;
3049 }
3050
3051 // IE8 doesn't have nextElementSibling
3052 var elm = element.nextSibling;
3053 while (elm != null && elm.nodeType !== 1) {
3054 elm = elm.nextSibling;
3055 }
3056 return elm;
3451 return element.nextElementSibling;
30573452 },
30583453
30593454 find: function(element, selector) {
30703465
30713466 var dummyEvent, eventFnsCopy, handlerArgs;
30723467 var eventName = event.type || event;
3073 var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName];
3468 var expandoStore = jqLiteExpandoStore(element);
3469 var events = expandoStore && expandoStore.events;
3470 var eventFns = events && events[eventName];
30743471
30753472 if (eventFns) {
3076
30773473 // Create a dummy event to pass to the handlers
30783474 dummyEvent = {
30793475 preventDefault: function() { this.defaultPrevented = true; },
30803476 isDefaultPrevented: function() { return this.defaultPrevented === true; },
3477 stopImmediatePropagation: function() { this.immediatePropagationStopped = true; },
3478 isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; },
30813479 stopPropagation: noop,
30823480 type: eventName,
30833481 target: element
30933491 handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent];
30943492
30953493 forEach(eventFnsCopy, function(fn) {
3096 fn.apply(element, handlerArgs);
3494 if (!dummyEvent.isImmediatePropagationStopped()) {
3495 fn.apply(element, handlerArgs);
3496 }
30973497 });
3098
30993498 }
31003499 }
3101 }, function(fn, name){
3500 }, function(fn, name) {
31023501 /**
31033502 * chaining functions
31043503 */
31053504 JQLite.prototype[name] = function(arg1, arg2, arg3) {
31063505 var value;
3107 for(var i=0; i < this.length; i++) {
3506
3507 for (var i = 0, ii = this.length; i < ii; i++) {
31083508 if (isUndefined(value)) {
31093509 value = fn(this[i], arg1, arg2, arg3);
31103510 if (isDefined(value)) {
31233523 JQLite.prototype.unbind = JQLite.prototype.off;
31243524 });
31253525
3526
3527 // Provider for private $$jqLite service
3528 function $$jqLiteProvider() {
3529 this.$get = function $$jqLite() {
3530 return extend(JQLite, {
3531 hasClass: function(node, classes) {
3532 if (node.attr) node = node[0];
3533 return jqLiteHasClass(node, classes);
3534 },
3535 addClass: function(node, classes) {
3536 if (node.attr) node = node[0];
3537 return jqLiteAddClass(node, classes);
3538 },
3539 removeClass: function(node, classes) {
3540 if (node.attr) node = node[0];
3541 return jqLiteRemoveClass(node, classes);
3542 }
3543 });
3544 };
3545 }
3546
31263547 /**
31273548 * Computes a hash of an 'obj'.
31283549 * Hash of a:
31363557 * The resulting string key is in 'type:hashKey' format.
31373558 */
31383559 function hashKey(obj, nextUidFn) {
3139 var objType = typeof obj,
3140 key;
3141
3560 var key = obj && obj.$$hashKey;
3561
3562 if (key) {
3563 if (typeof key === 'function') {
3564 key = obj.$$hashKey();
3565 }
3566 return key;
3567 }
3568
3569 var objType = typeof obj;
31423570 if (objType == 'function' || (objType == 'object' && obj !== null)) {
3143 if (typeof (key = obj.$$hashKey) == 'function') {
3144 // must invoke on object to keep the right this
3145 key = obj.$$hashKey();
3146 } else if (key === undefined) {
3147 key = obj.$$hashKey = (nextUidFn || nextUid)();
3148 }
3571 key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)();
31493572 } else {
3150 key = obj;
3573 key = objType + ':' + obj;
31513574 }
31523575
3153 return objType + ':' + key;
3576 return key;
31543577 }
31553578
31563579 /**
31943617 }
31953618 };
31963619
3620 var $$HashMapProvider = [function() {
3621 this.$get = [function() {
3622 return HashMap;
3623 }];
3624 }];
3625
31973626 /**
31983627 * @ngdoc function
31993628 * @module ng
32013630 * @kind function
32023631 *
32033632 * @description
3204 * Creates an injector function that can be used for retrieving services as well as for
3633 * Creates an injector object that can be used for retrieving services as well as for
32053634 * dependency injection (see {@link guide/di dependency injection}).
32063635 *
3207
32083636 * @param {Array.<string|Function>} modules A list of module functions or their aliases. See
3209 * {@link angular.module}. The `ng` module must be explicitly added.
3210 * @returns {function()} Injector function. See {@link auto.$injector $injector}.
3637 * {@link angular.module}. The `ng` module must be explicitly added.
3638 * @param {boolean=} [strictDi=false] Whether the injector should be in strict mode, which
3639 * disallows argument name annotation inference.
3640 * @returns {injector} Injector object. See {@link auto.$injector $injector}.
32113641 *
32123642 * @example
32133643 * Typical usage
32173647 *
32183648 * // use the injector to kick off your application
32193649 * // use the type inference to auto inject arguments, or use implicit injection
3220 * $injector.invoke(function($rootScope, $compile, $document){
3650 * $injector.invoke(function($rootScope, $compile, $document) {
32213651 * $compile($document)($rootScope);
32223652 * $rootScope.$digest();
32233653 * });
32603690 var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
32613691 var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
32623692 var $injectorMinErr = minErr('$injector');
3263 function annotate(fn) {
3693
3694 function anonFn(fn) {
3695 // For anonymous functions, showing at the very least the function signature can help in
3696 // debugging.
3697 var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
3698 args = fnText.match(FN_ARGS);
3699 if (args) {
3700 return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
3701 }
3702 return 'fn';
3703 }
3704
3705 function annotate(fn, strictDi, name) {
32643706 var $inject,
32653707 fnText,
32663708 argDecl,
32703712 if (!($inject = fn.$inject)) {
32713713 $inject = [];
32723714 if (fn.length) {
3715 if (strictDi) {
3716 if (!isString(name) || !name) {
3717 name = fn.name || anonFn(fn);
3718 }
3719 throw $injectorMinErr('strictdi',
3720 '{0} is not using explicit annotation and cannot be invoked in strict mode', name);
3721 }
32733722 fnText = fn.toString().replace(STRIP_COMMENTS, '');
32743723 argDecl = fnText.match(FN_ARGS);
3275 forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
3276 arg.replace(FN_ARG, function(all, underscore, name){
3724 forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
3725 arg.replace(FN_ARG, function(all, underscore, name) {
32773726 $inject.push(name);
32783727 });
32793728 });
32953744 /**
32963745 * @ngdoc service
32973746 * @name $injector
3298 * @kind function
32993747 *
33003748 * @description
33013749 *
33083756 * ```js
33093757 * var $injector = angular.injector();
33103758 * expect($injector.get('$injector')).toBe($injector);
3311 * expect($injector.invoke(function($injector){
3759 * expect($injector.invoke(function($injector) {
33123760 * return $injector;
3313 * }).toBe($injector);
3761 * })).toBe($injector);
33143762 * ```
33153763 *
33163764 * # Injection Function Annotation
33343782 * ## Inference
33353783 *
33363784 * In JavaScript calling `toString()` on a function returns the function definition. The definition
3337 * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with
3338 * minification, and obfuscation tools since these tools change the argument names.
3785 * can then be parsed and the function arguments can be extracted. This method of discovering
3786 * annotations is disallowed when the injector is in strict mode.
3787 * *NOTE:* This does not work with minification, and obfuscation tools since these tools change the
3788 * argument names.
33393789 *
33403790 * ## `$inject` Annotation
33413791 * By adding an `$inject` property onto a function the injection parameters can be specified.
33523802 * Return an instance of the service.
33533803 *
33543804 * @param {string} name The name of the instance to retrieve.
3805 * @param {string=} caller An optional string to provide the origin of the function call for error messages.
33553806 * @return {*} The instance.
33563807 */
33573808
33623813 * @description
33633814 * Invoke the method and supply the method arguments from the `$injector`.
33643815 *
3365 * @param {!Function} fn The function to invoke. Function parameters are injected according to the
3366 * {@link guide/di $inject Annotation} rules.
3816 * @param {Function|Array.<string|Function>} fn The injectable function to invoke. Function parameters are
3817 * injected according to the {@link guide/di $inject Annotation} rules.
33673818 * @param {Object=} self The `this` for the invoked method.
33683819 * @param {Object=} locals Optional object. If preset then any argument names are read from this
33693820 * object first, before the `$injector` is consulted.
33773828 * @description
33783829 * Allows the user to query if the particular service exists.
33793830 *
3380 * @param {string} Name of the service to query.
3381 * @returns {boolean} returns true if injector has given service.
3831 * @param {string} name Name of the service to query.
3832 * @returns {boolean} `true` if injector has given service.
33823833 */
33833834
33843835 /**
34193870 * // Then
34203871 * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']);
34213872 * ```
3873 *
3874 * You can disallow this method by using strict injection mode.
34223875 *
34233876 * This method does not work with code minification / obfuscation. For this reason the following
34243877 * annotation strategies are supported.
34713924 *
34723925 * @param {Function|Array.<string|Function>} fn Function for which dependent service names need to
34733926 * be retrieved as described above.
3927 *
3928 * @param {boolean=} [strictDi=false] Disallow argument name annotation inference.
34743929 *
34753930 * @returns {Array.<string>} The names of the services which the function requires.
34763931 */
36264081 * configure your service in a provider.
36274082 *
36284083 * @param {string} name The name of the instance.
3629 * @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand
3630 * for `$provide.provider(name, {$get: $getFn})`.
4084 * @param {Function|Array.<string|Function>} $getFn The injectable $getFn for the instance creation.
4085 * Internally this is a short hand for `$provide.provider(name, {$get: $getFn})`.
36314086 * @returns {Object} registered provider instance
36324087 *
36334088 * @example
36624117 * as a type/class.
36634118 *
36644119 * @param {string} name The name of the instance.
3665 * @param {Function} constructor A class (constructor function) that will be instantiated.
4120 * @param {Function|Array.<string|Function>} constructor An injectable class (constructor function)
4121 * that will be instantiated.
36664122 * @returns {Object} registered provider instance
36674123 *
36684124 * @example
37614217 * object which replaces or wraps and delegates to the original service.
37624218 *
37634219 * @param {string} name The name of the service to decorate.
3764 * @param {function()} decorator This function will be invoked when the service needs to be
4220 * @param {Function|Array.<string|Function>} decorator This function will be invoked when the service needs to be
37654221 * instantiated and should return the decorated service instance. The function is called using
37664222 * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable.
37674223 * Local injection arguments:
37814237 */
37824238
37834239
3784 function createInjector(modulesToLoad) {
4240 function createInjector(modulesToLoad, strictDi) {
4241 strictDi = (strictDi === true);
37854242 var INSTANTIATING = {},
37864243 providerSuffix = 'Provider',
37874244 path = [],
37974254 }
37984255 },
37994256 providerInjector = (providerCache.$injector =
3800 createInternalInjector(providerCache, function() {
4257 createInternalInjector(providerCache, function(serviceName, caller) {
4258 if (angular.isString(caller)) {
4259 path.push(caller);
4260 }
38014261 throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
38024262 })),
38034263 instanceCache = {},
38044264 instanceInjector = (instanceCache.$injector =
3805 createInternalInjector(instanceCache, function(servicename) {
3806 var provider = providerInjector.get(servicename + providerSuffix);
3807 return instanceInjector.invoke(provider.$get, provider);
4265 createInternalInjector(instanceCache, function(serviceName, caller) {
4266 var provider = providerInjector.get(serviceName + providerSuffix, caller);
4267 return instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
38084268 }));
38094269
38104270
3811 forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });
4271 forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); });
38124272
38134273 return instanceInjector;
38144274
38374297 return providerCache[name + providerSuffix] = provider_;
38384298 }
38394299
3840 function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); }
4300 function enforceReturnValue(name, factory) {
4301 return function enforcedReturnValue() {
4302 var result = instanceInjector.invoke(factory, this);
4303 if (isUndefined(result)) {
4304 throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name);
4305 }
4306 return result;
4307 };
4308 }
4309
4310 function factory(name, factoryFn, enforce) {
4311 return provider(name, {
4312 $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn
4313 });
4314 }
38414315
38424316 function service(name, constructor) {
38434317 return factory(name, ['$injector', function($injector) {
38454319 }]);
38464320 }
38474321
3848 function value(name, val) { return factory(name, valueFn(val)); }
4322 function value(name, val) { return factory(name, valueFn(val), false); }
38494323
38504324 function constant(name, value) {
38514325 assertNotHasOwnProperty(name, 'constant');
38664340 ////////////////////////////////////
38674341 // Module Loading
38684342 ////////////////////////////////////
3869 function loadModules(modulesToLoad){
3870 var runBlocks = [], moduleFn, invokeQueue, i, ii;
4343 function loadModules(modulesToLoad) {
4344 var runBlocks = [], moduleFn;
38714345 forEach(modulesToLoad, function(module) {
38724346 if (loadedModules.get(module)) return;
38734347 loadedModules.put(module, true);
4348
4349 function runInvokeQueue(queue) {
4350 var i, ii;
4351 for (i = 0, ii = queue.length; i < ii; i++) {
4352 var invokeArgs = queue[i],
4353 provider = providerInjector.get(invokeArgs[0]);
4354
4355 provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
4356 }
4357 }
38744358
38754359 try {
38764360 if (isString(module)) {
38774361 moduleFn = angularModule(module);
38784362 runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
3879
3880 for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) {
3881 var invokeArgs = invokeQueue[i],
3882 provider = providerInjector.get(invokeArgs[0]);
3883
3884 provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
3885 }
4363 runInvokeQueue(moduleFn._invokeQueue);
4364 runInvokeQueue(moduleFn._configBlocks);
38864365 } else if (isFunction(module)) {
38874366 runBlocks.push(providerInjector.invoke(module));
38884367 } else if (isArray(module)) {
39154394
39164395 function createInternalInjector(cache, factory) {
39174396
3918 function getService(serviceName) {
4397 function getService(serviceName, caller) {
39194398 if (cache.hasOwnProperty(serviceName)) {
39204399 if (cache[serviceName] === INSTANTIATING) {
39214400 throw $injectorMinErr('cdep', 'Circular dependency found: {0}',
39264405 try {
39274406 path.unshift(serviceName);
39284407 cache[serviceName] = INSTANTIATING;
3929 return cache[serviceName] = factory(serviceName);
4408 return cache[serviceName] = factory(serviceName, caller);
39304409 } catch (err) {
39314410 if (cache[serviceName] === INSTANTIATING) {
39324411 delete cache[serviceName];
39384417 }
39394418 }
39404419
3941 function invoke(fn, self, locals){
4420 function invoke(fn, self, locals, serviceName) {
4421 if (typeof locals === 'string') {
4422 serviceName = locals;
4423 locals = null;
4424 }
4425
39424426 var args = [],
3943 $inject = annotate(fn),
4427 $inject = createInjector.$$annotate(fn, strictDi, serviceName),
39444428 length, i,
39454429 key;
39464430
3947 for(i = 0, length = $inject.length; i < length; i++) {
4431 for (i = 0, length = $inject.length; i < length; i++) {
39484432 key = $inject[i];
39494433 if (typeof key !== 'string') {
39504434 throw $injectorMinErr('itkn',
39534437 args.push(
39544438 locals && locals.hasOwnProperty(key)
39554439 ? locals[key]
3956 : getService(key)
4440 : getService(key, serviceName)
39574441 );
39584442 }
39594443 if (isArray(fn)) {
39654449 return fn.apply(self, args);
39664450 }
39674451
3968 function instantiate(Type, locals) {
3969 var Constructor = function() {},
3970 instance, returnedValue;
3971
4452 function instantiate(Type, locals, serviceName) {
39724453 // Check if Type is annotated and use just the given function at n-1 as parameter
39734454 // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
3974 Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype;
3975 instance = new Constructor();
3976 returnedValue = invoke(Type, instance, locals);
4455 // Object creation: http://jsperf.com/create-constructor/2
4456 var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null);
4457 var returnedValue = invoke(Type, instance, locals, serviceName);
39774458
39784459 return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
39794460 }
39824463 invoke: invoke,
39834464 instantiate: instantiate,
39844465 get: getService,
3985 annotate: annotate,
4466 annotate: createInjector.$$annotate,
39864467 has: function(name) {
39874468 return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
39884469 }
39904471 }
39914472 }
39924473
4474 createInjector.$$annotate = annotate;
4475
39934476 /**
3994 * @ngdoc service
3995 * @name $anchorScroll
3996 * @kind function
3997 * @requires $window
3998 * @requires $location
3999 * @requires $rootScope
4477 * @ngdoc provider
4478 * @name $anchorScrollProvider
40004479 *
40014480 * @description
4002 * When called, it checks current value of `$location.hash()` and scrolls to the related element,
4003 * according to rules specified in
4004 * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
4005 *
4006 * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor.
4007 * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`.
4008 *
4009 * @example
4010 <example>
4011 <file name="index.html">
4012 <div id="scrollArea" ng-controller="ScrollCtrl">
4013 <a ng-click="gotoBottom()">Go to bottom</a>
4014 <a id="bottom"></a> You're at the bottom!
4015 </div>
4016 </file>
4017 <file name="script.js">
4018 function ScrollCtrl($scope, $location, $anchorScroll) {
4019 $scope.gotoBottom = function (){
4020 // set the location.hash to the id of
4021 // the element you wish to scroll to.
4022 $location.hash('bottom');
4023
4024 // call $anchorScroll()
4025 $anchorScroll();
4026 };
4027 }
4028 </file>
4029 <file name="style.css">
4030 #scrollArea {
4031 height: 350px;
4032 overflow: auto;
4033 }
4034
4035 #bottom {
4036 display: block;
4037 margin-top: 2000px;
4038 }
4039 </file>
4040 </example>
4481 * Use `$anchorScrollProvider` to disable automatic scrolling whenever
4482 * {@link ng.$location#hash $location.hash()} changes.
40414483 */
40424484 function $AnchorScrollProvider() {
40434485
40444486 var autoScrollingEnabled = true;
40454487
4488 /**
4489 * @ngdoc method
4490 * @name $anchorScrollProvider#disableAutoScrolling
4491 *
4492 * @description
4493 * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to
4494 * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.<br />
4495 * Use this method to disable automatic scrolling.
4496 *
4497 * If automatic scrolling is disabled, one must explicitly call
4498 * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the
4499 * current hash.
4500 */
40464501 this.disableAutoScrolling = function() {
40474502 autoScrollingEnabled = false;
40484503 };
40494504
4505 /**
4506 * @ngdoc service
4507 * @name $anchorScroll
4508 * @kind function
4509 * @requires $window
4510 * @requires $location
4511 * @requires $rootScope
4512 *
4513 * @description
4514 * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
4515 * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
4516 * in the
4517 * [HTML5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
4518 *
4519 * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
4520 * match any anchor whenever it changes. This can be disabled by calling
4521 * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}.
4522 *
4523 * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a
4524 * vertical scroll-offset (either fixed or dynamic).
4525 *
4526 * @param {string=} hash The hash specifying the element to scroll to. If omitted, the value of
4527 * {@link ng.$location#hash $location.hash()} will be used.
4528 *
4529 * @property {(number|function|jqLite)} yOffset
4530 * If set, specifies a vertical scroll-offset. This is often useful when there are fixed
4531 * positioned elements at the top of the page, such as navbars, headers etc.
4532 *
4533 * `yOffset` can be specified in various ways:
4534 * - **number**: A fixed number of pixels to be used as offset.<br /><br />
4535 * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return
4536 * a number representing the offset (in pixels).<br /><br />
4537 * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from
4538 * the top of the page to the element's bottom will be used as offset.<br />
4539 * **Note**: The element will be taken into account only as long as its `position` is set to
4540 * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust
4541 * their height and/or positioning according to the viewport's size.
4542 *
4543 * <br />
4544 * <div class="alert alert-warning">
4545 * In order for `yOffset` to work properly, scrolling should take place on the document's root and
4546 * not some child element.
4547 * </div>
4548 *
4549 * @example
4550 <example module="anchorScrollExample">
4551 <file name="index.html">
4552 <div id="scrollArea" ng-controller="ScrollController">
4553 <a ng-click="gotoBottom()">Go to bottom</a>
4554 <a id="bottom"></a> You're at the bottom!
4555 </div>
4556 </file>
4557 <file name="script.js">
4558 angular.module('anchorScrollExample', [])
4559 .controller('ScrollController', ['$scope', '$location', '$anchorScroll',
4560 function ($scope, $location, $anchorScroll) {
4561 $scope.gotoBottom = function() {
4562 // set the location.hash to the id of
4563 // the element you wish to scroll to.
4564 $location.hash('bottom');
4565
4566 // call $anchorScroll()
4567 $anchorScroll();
4568 };
4569 }]);
4570 </file>
4571 <file name="style.css">
4572 #scrollArea {
4573 height: 280px;
4574 overflow: auto;
4575 }
4576
4577 #bottom {
4578 display: block;
4579 margin-top: 2000px;
4580 }
4581 </file>
4582 </example>
4583 *
4584 * <hr />
4585 * The example below illustrates the use of a vertical scroll-offset (specified as a fixed value).
4586 * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details.
4587 *
4588 * @example
4589 <example module="anchorScrollOffsetExample">
4590 <file name="index.html">
4591 <div class="fixed-header" ng-controller="headerCtrl">
4592 <a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
4593 Go to anchor {{x}}
4594 </a>
4595 </div>
4596 <div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]">
4597 Anchor {{x}} of 5
4598 </div>
4599 </file>
4600 <file name="script.js">
4601 angular.module('anchorScrollOffsetExample', [])
4602 .run(['$anchorScroll', function($anchorScroll) {
4603 $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels
4604 }])
4605 .controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
4606 function ($anchorScroll, $location, $scope) {
4607 $scope.gotoAnchor = function(x) {
4608 var newHash = 'anchor' + x;
4609 if ($location.hash() !== newHash) {
4610 // set the $location.hash to `newHash` and
4611 // $anchorScroll will automatically scroll to it
4612 $location.hash('anchor' + x);
4613 } else {
4614 // call $anchorScroll() explicitly,
4615 // since $location.hash hasn't changed
4616 $anchorScroll();
4617 }
4618 };
4619 }
4620 ]);
4621 </file>
4622 <file name="style.css">
4623 body {
4624 padding-top: 50px;
4625 }
4626
4627 .anchor {
4628 border: 2px dashed DarkOrchid;
4629 padding: 10px 10px 200px 10px;
4630 }
4631
4632 .fixed-header {
4633 background-color: rgba(0, 0, 0, 0.2);
4634 height: 50px;
4635 position: fixed;
4636 top: 0; left: 0; right: 0;
4637 }
4638
4639 .fixed-header > a {
4640 display: inline-block;
4641 margin: 5px 15px;
4642 }
4643 </file>
4644 </example>
4645 */
40504646 this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) {
40514647 var document = $window.document;
40524648
4053 // helper function to get first anchor from a NodeList
4054 // can't use filter.filter, as it accepts only instances of Array
4055 // and IE can't convert NodeList to an array using [].slice
4056 // TODO(vojta): use filter if we change it to accept lists as well
4649 // Helper function to get first anchor from a NodeList
4650 // (using `Array#some()` instead of `angular#forEach()` since it's more performant
4651 // and working in all supported browsers.)
40574652 function getFirstAnchor(list) {
40584653 var result = null;
4059 forEach(list, function(element) {
4060 if (!result && lowercase(element.nodeName) === 'a') result = element;
4654 Array.prototype.some.call(list, function(element) {
4655 if (nodeName_(element) === 'a') {
4656 result = element;
4657 return true;
4658 }
40614659 });
40624660 return result;
40634661 }
40644662
4065 function scroll() {
4066 var hash = $location.hash(), elm;
4663 function getYOffset() {
4664
4665 var offset = scroll.yOffset;
4666
4667 if (isFunction(offset)) {
4668 offset = offset();
4669 } else if (isElement(offset)) {
4670 var elem = offset[0];
4671 var style = $window.getComputedStyle(elem);
4672 if (style.position !== 'fixed') {
4673 offset = 0;
4674 } else {
4675 offset = elem.getBoundingClientRect().bottom;
4676 }
4677 } else if (!isNumber(offset)) {
4678 offset = 0;
4679 }
4680
4681 return offset;
4682 }
4683
4684 function scrollTo(elem) {
4685 if (elem) {
4686 elem.scrollIntoView();
4687
4688 var offset = getYOffset();
4689
4690 if (offset) {
4691 // `offset` is the number of pixels we should scroll UP in order to align `elem` properly.
4692 // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the
4693 // top of the viewport.
4694 //
4695 // IF the number of pixels from the top of `elem` to the end of the page's content is less
4696 // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some
4697 // way down the page.
4698 //
4699 // This is often the case for elements near the bottom of the page.
4700 //
4701 // In such cases we do not need to scroll the whole `offset` up, just the difference between
4702 // the top of the element and the offset, which is enough to align the top of `elem` at the
4703 // desired position.
4704 var elemTop = elem.getBoundingClientRect().top;
4705 $window.scrollBy(0, elemTop - offset);
4706 }
4707 } else {
4708 $window.scrollTo(0, 0);
4709 }
4710 }
4711
4712 function scroll(hash) {
4713 hash = isString(hash) ? hash : $location.hash();
4714 var elm;
40674715
40684716 // empty hash, scroll to the top of the page
4069 if (!hash) $window.scrollTo(0, 0);
4717 if (!hash) scrollTo(null);
40704718
40714719 // element with given id
4072 else if ((elm = document.getElementById(hash))) elm.scrollIntoView();
4720 else if ((elm = document.getElementById(hash))) scrollTo(elm);
40734721
40744722 // first anchor with given name :-D
4075 else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView();
4723 else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm);
40764724
40774725 // no element and hash == 'top', scroll to the top of the page
4078 else if (hash === 'top') $window.scrollTo(0, 0);
4726 else if (hash === 'top') scrollTo(null);
40794727 }
40804728
40814729 // does not scroll when user clicks on anchor link that is currently on
40824730 // (no url change, no $location.hash() change), browser native does scroll
40834731 if (autoScrollingEnabled) {
40844732 $rootScope.$watch(function autoScrollWatch() {return $location.hash();},
4085 function autoScrollWatchAction() {
4086 $rootScope.$evalAsync(scroll);
4733 function autoScrollWatchAction(newVal, oldVal) {
4734 // skip the initial scroll if $location.hash is empty
4735 if (newVal === oldVal && newVal === '') return;
4736
4737 jqLiteDocumentLoaded(function() {
4738 $rootScope.$evalAsync(scroll);
4739 });
40874740 });
40884741 }
40894742
40924745 }
40934746
40944747 var $animateMinErr = minErr('$animate');
4748 var ELEMENT_NODE = 1;
4749 var NG_ANIMATE_CLASSNAME = 'ng-animate';
4750
4751 function mergeClasses(a,b) {
4752 if (!a && !b) return '';
4753 if (!a) return b;
4754 if (!b) return a;
4755 if (isArray(a)) a = a.join(' ');
4756 if (isArray(b)) b = b.join(' ');
4757 return a + ' ' + b;
4758 }
4759
4760 function extractElementNode(element) {
4761 for (var i = 0; i < element.length; i++) {
4762 var elm = element[i];
4763 if (elm.nodeType === ELEMENT_NODE) {
4764 return elm;
4765 }
4766 }
4767 }
4768
4769 function splitClasses(classes) {
4770 if (isString(classes)) {
4771 classes = classes.split(' ');
4772 }
4773
4774 // Use createMap() to prevent class assumptions involving property names in
4775 // Object.prototype
4776 var obj = createMap();
4777 forEach(classes, function(klass) {
4778 // sometimes the split leaves empty string values
4779 // incase extra spaces were applied to the options
4780 if (klass.length) {
4781 obj[klass] = true;
4782 }
4783 });
4784 return obj;
4785 }
4786
4787 // if any other type of options value besides an Object value is
4788 // passed into the $animate.method() animation then this helper code
4789 // will be run which will ignore it. While this patch is not the
4790 // greatest solution to this, a lot of existing plugins depend on
4791 // $animate to either call the callback (< 1.2) or return a promise
4792 // that can be changed. This helper function ensures that the options
4793 // are wiped clean incase a callback function is provided.
4794 function prepareAnimateOptions(options) {
4795 return isObject(options)
4796 ? options
4797 : {};
4798 }
4799
4800 var $$CoreAnimateRunnerProvider = function() {
4801 this.$get = ['$q', '$$rAF', function($q, $$rAF) {
4802 function AnimateRunner() {}
4803 AnimateRunner.all = noop;
4804 AnimateRunner.chain = noop;
4805 AnimateRunner.prototype = {
4806 end: noop,
4807 cancel: noop,
4808 resume: noop,
4809 pause: noop,
4810 complete: noop,
4811 then: function(pass, fail) {
4812 return $q(function(resolve) {
4813 $$rAF(function() {
4814 resolve();
4815 });
4816 }).then(pass, fail);
4817 }
4818 };
4819 return AnimateRunner;
4820 }];
4821 };
4822
4823 // this is prefixed with Core since it conflicts with
4824 // the animateQueueProvider defined in ngAnimate/animateQueue.js
4825 var $$CoreAnimateQueueProvider = function() {
4826 var postDigestQueue = new HashMap();
4827 var postDigestElements = [];
4828
4829 this.$get = ['$$AnimateRunner', '$rootScope',
4830 function($$AnimateRunner, $rootScope) {
4831 return {
4832 enabled: noop,
4833 on: noop,
4834 off: noop,
4835 pin: noop,
4836
4837 push: function(element, event, options, domOperation) {
4838 domOperation && domOperation();
4839
4840 options = options || {};
4841 options.from && element.css(options.from);
4842 options.to && element.css(options.to);
4843
4844 if (options.addClass || options.removeClass) {
4845 addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
4846 }
4847
4848 return new $$AnimateRunner(); // jshint ignore:line
4849 }
4850 };
4851
4852 function addRemoveClassesPostDigest(element, add, remove) {
4853 var data = postDigestQueue.get(element);
4854 var classVal;
4855
4856 if (!data) {
4857 postDigestQueue.put(element, data = {});
4858 postDigestElements.push(element);
4859 }
4860
4861 if (add) {
4862 forEach(add.split(' '), function(className) {
4863 if (className) {
4864 data[className] = true;
4865 }
4866 });
4867 }
4868
4869 if (remove) {
4870 forEach(remove.split(' '), function(className) {
4871 if (className) {
4872 data[className] = false;
4873 }
4874 });
4875 }
4876
4877 if (postDigestElements.length > 1) return;
4878
4879 $rootScope.$$postDigest(function() {
4880 forEach(postDigestElements, function(element) {
4881 var data = postDigestQueue.get(element);
4882 if (data) {
4883 var existing = splitClasses(element.attr('class'));
4884 var toAdd = '';
4885 var toRemove = '';
4886 forEach(data, function(status, className) {
4887 var hasClass = !!existing[className];
4888 if (status !== hasClass) {
4889 if (status) {
4890 toAdd += (toAdd.length ? ' ' : '') + className;
4891 } else {
4892 toRemove += (toRemove.length ? ' ' : '') + className;
4893 }
4894 }
4895 });
4896
4897 forEach(element, function(elm) {
4898 toAdd && jqLiteAddClass(elm, toAdd);
4899 toRemove && jqLiteRemoveClass(elm, toRemove);
4900 });
4901 postDigestQueue.remove(element);
4902 }
4903 });
4904
4905 postDigestElements.length = 0;
4906 });
4907 }
4908 }];
4909 };
40954910
40964911 /**
40974912 * @ngdoc provider
40994914 *
41004915 * @description
41014916 * Default implementation of $animate that doesn't perform any animations, instead just
4102 * synchronously performs DOM
4103 * updates and calls done() callbacks.
4104 *
4105 * In order to enable animations the ngAnimate module has to be loaded.
4106 *
4107 * To see the functional implementation check out src/ngAnimate/animate.js
4917 * synchronously performs DOM updates and resolves the returned runner promise.
4918 *
4919 * In order to enable animations the `ngAnimate` module has to be loaded.
4920 *
4921 * To see the functional implementation check out `src/ngAnimate/animate.js`.
41084922 */
41094923 var $AnimateProvider = ['$provide', function($provide) {
4110
4111
4112 this.$$selectors = {};
4113
4114
4115 /**
4924 var provider = this;
4925
4926 this.$$registeredAnimations = Object.create(null);
4927
4928 /**
41164929 * @ngdoc method
41174930 * @name $animateProvider#register
41184931 *
41214934 * animation object which contains callback functions for each event that is expected to be
41224935 * animated.
41234936 *
4124 * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction`
4125 * must be called once the element animation is complete. If a function is returned then the
4126 * animation service will use this function to cancel the animation whenever a cancel event is
4127 * triggered.
4128 *
4937 * * `eventFn`: `function(element, ... , doneFunction, options)`
4938 * The element to animate, the `doneFunction` and the options fed into the animation. Depending
4939 * on the type of animation additional arguments will be injected into the animation function. The
4940 * list below explains the function signatures for the different animation methods:
4941 *
4942 * - setClass: function(element, addedClasses, removedClasses, doneFunction, options)
4943 * - addClass: function(element, addedClasses, doneFunction, options)
4944 * - removeClass: function(element, removedClasses, doneFunction, options)
4945 * - enter, leave, move: function(element, doneFunction, options)
4946 * - animate: function(element, fromStyles, toStyles, doneFunction, options)
4947 *
4948 * Make sure to trigger the `doneFunction` once the animation is fully complete.
41294949 *
41304950 * ```js
41314951 * return {
4132 * eventFn : function(element, done) {
4133 * //code to run the animation
4134 * //once complete, then run done()
4135 * return function cancellationFunction() {
4136 * //code to cancel the animation
4137 * }
4138 * }
4139 * }
4952 * //enter, leave, move signature
4953 * eventFn : function(element, done, options) {
4954 * //code to run the animation
4955 * //once complete, then run done()
4956 * return function endFunction(wasCancelled) {
4957 * //code to cancel the animation
4958 * }
4959 * }
4960 * }
41404961 * ```
41414962 *
4142 * @param {string} name The name of the animation.
4963 * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to).
41434964 * @param {Function} factory The factory function that will be executed to return the animation
41444965 * object.
41454966 */
41464967 this.register = function(name, factory) {
4968 if (name && name.charAt(0) !== '.') {
4969 throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name);
4970 }
4971
41474972 var key = name + '-animation';
4148 if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel',
4149 "Expecting class selector starting with '.' got '{0}'.", name);
4150 this.$$selectors[name.substr(1)] = key;
4973 provider.$$registeredAnimations[name.substr(1)] = key;
41514974 $provide.factory(key, factory);
41524975 };
41534976
41584981 * @description
41594982 * Sets and/or returns the CSS class regular expression that is checked when performing
41604983 * an animation. Upon bootstrap the classNameFilter value is not set at all and will
4161 * therefore enable $animate to attempt to perform an animation on any element.
4162 * When setting the classNameFilter value, animations will only be performed on elements
4984 * therefore enable $animate to attempt to perform an animation on any element that is triggered.
4985 * When setting the `classNameFilter` value, animations will only be performed on elements
41634986 * that successfully match the filter expression. This in turn can boost performance
41644987 * for low-powered devices as well as applications containing a lot of structural operations.
41654988 * @param {RegExp=} expression The className expression which will be checked against all animations
41664989 * @return {RegExp} The current CSS className expression value. If null then there is no expression value
41674990 */
41684991 this.classNameFilter = function(expression) {
4169 if(arguments.length === 1) {
4992 if (arguments.length === 1) {
41704993 this.$$classNameFilter = (expression instanceof RegExp) ? expression : null;
4994 if (this.$$classNameFilter) {
4995 var reservedRegex = new RegExp("(\\s+|\\/)" + NG_ANIMATE_CLASSNAME + "(\\s+|\\/)");
4996 if (reservedRegex.test(this.$$classNameFilter.toString())) {
4997 throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
4998
4999 }
5000 }
41715001 }
41725002 return this.$$classNameFilter;
41735003 };
41745004
4175 this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) {
4176
4177 function async(fn) {
4178 fn && $$asyncCallback(fn);
5005 this.$get = ['$$animateQueue', function($$animateQueue) {
5006 function domInsert(element, parentElement, afterElement) {
5007 // if for some reason the previous element was removed
5008 // from the dom sometime before this code runs then let's
5009 // just stick to using the parent element as the anchor
5010 if (afterElement) {
5011 var afterNode = extractElementNode(afterElement);
5012 if (afterNode && !afterNode.parentNode && !afterNode.previousElementSibling) {
5013 afterElement = null;
5014 }
5015 }
5016 afterElement ? afterElement.after(element) : parentElement.prepend(element);
41795017 }
41805018
41815019 /**
4182 *
41835020 * @ngdoc service
41845021 * @name $animate
4185 * @description The $animate service provides rudimentary DOM manipulation functions to
4186 * insert, remove and move elements within the DOM, as well as adding and removing classes.
4187 * This service is the core service used by the ngAnimate $animator service which provides
4188 * high-level animation hooks for CSS and JavaScript.
5022 * @description The $animate service exposes a series of DOM utility methods that provide support
5023 * for animation hooks. The default behavior is the application of DOM operations, however,
5024 * when an animation is detected (and animations are enabled), $animate will do the heavy lifting
5025 * to ensure that animation runs with the triggered DOM operation.
41895026 *
4190 * $animate is available in the AngularJS core, however, the ngAnimate module must be included
4191 * to enable full out animation support. Otherwise, $animate will only perform simple DOM
4192 * manipulation operations.
5027 * By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't
5028 * included and only when it is active then the animation hooks that `$animate` triggers will be
5029 * functional. Once active then all structural `ng-` directives will trigger animations as they perform
5030 * their DOM-related operations (enter, leave and move). Other directives such as `ngClass`,
5031 * `ngShow`, `ngHide` and `ngMessages` also provide support for animations.
41935032 *
4194 * To learn more about enabling animation support, click here to visit the {@link ngAnimate
4195 * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service
4196 * page}.
5033 * It is recommended that the`$animate` service is always used when executing DOM-related procedures within directives.
5034 *
5035 * To learn more about enabling animation support, click here to visit the
5036 * {@link ngAnimate ngAnimate module page}.
41975037 */
41985038 return {
5039 // we don't call it directly since non-existant arguments may
5040 // be interpreted as null within the sub enabled function
5041
5042 /**
5043 *
5044 * @ngdoc method
5045 * @name $animate#on
5046 * @kind function
5047 * @description Sets up an event listener to fire whenever the animation event (enter, leave, move, etc...)
5048 * has fired on the given element or among any of its children. Once the listener is fired, the provided callback
5049 * is fired with the following params:
5050 *
5051 * ```js
5052 * $animate.on('enter', container,
5053 * function callback(element, phase) {
5054 * // cool we detected an enter animation within the container
5055 * }
5056 * );
5057 * ```
5058 *
5059 * @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...)
5060 * @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself
5061 * as well as among its children
5062 * @param {Function} callback the callback function that will be fired when the listener is triggered
5063 *
5064 * The arguments present in the callback function are:
5065 * * `element` - The captured DOM element that the animation was fired on.
5066 * * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends).
5067 */
5068 on: $$animateQueue.on,
5069
5070 /**
5071 *
5072 * @ngdoc method
5073 * @name $animate#off
5074 * @kind function
5075 * @description Deregisters an event listener based on the event which has been associated with the provided element. This method
5076 * can be used in three different ways depending on the arguments:
5077 *
5078 * ```js
5079 * // remove all the animation event listeners listening for `enter`
5080 * $animate.off('enter');
5081 *
5082 * // remove all the animation event listeners listening for `enter` on the given element and its children
5083 * $animate.off('enter', container);
5084 *
5085 * // remove the event listener function provided by `listenerFn` that is set
5086 * // to listen for `enter` on the given `element` as well as its children
5087 * $animate.off('enter', container, callback);
5088 * ```
5089 *
5090 * @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...)
5091 * @param {DOMElement=} container the container element the event listener was placed on
5092 * @param {Function=} callback the callback function that was registered as the listener
5093 */
5094 off: $$animateQueue.off,
5095
5096 /**
5097 * @ngdoc method
5098 * @name $animate#pin
5099 * @kind function
5100 * @description Associates the provided element with a host parent element to allow the element to be animated even if it exists
5101 * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the
5102 * element despite being outside the realm of the application or within another application. Say for example if the application
5103 * was bootstrapped on an element that is somewhere inside of the `<body>` tag, but we wanted to allow for an element to be situated
5104 * as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind
5105 * that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association.
5106 *
5107 * Note that this feature is only active when the `ngAnimate` module is used.
5108 *
5109 * @param {DOMElement} element the external element that will be pinned
5110 * @param {DOMElement} parentElement the host parent element that will be associated with the external element
5111 */
5112 pin: $$animateQueue.pin,
5113
5114 /**
5115 *
5116 * @ngdoc method
5117 * @name $animate#enabled
5118 * @kind function
5119 * @description Used to get and set whether animations are enabled or not on the entire application or on an element and its children. This
5120 * function can be called in four ways:
5121 *
5122 * ```js
5123 * // returns true or false
5124 * $animate.enabled();
5125 *
5126 * // changes the enabled state for all animations
5127 * $animate.enabled(false);
5128 * $animate.enabled(true);
5129 *
5130 * // returns true or false if animations are enabled for an element
5131 * $animate.enabled(element);
5132 *
5133 * // changes the enabled state for an element and its children
5134 * $animate.enabled(element, true);
5135 * $animate.enabled(element, false);
5136 * ```
5137 *
5138 * @param {DOMElement=} element the element that will be considered for checking/setting the enabled state
5139 * @param {boolean=} enabled whether or not the animations will be enabled for the element
5140 *
5141 * @return {boolean} whether or not animations are enabled
5142 */
5143 enabled: $$animateQueue.enabled,
5144
5145 /**
5146 * @ngdoc method
5147 * @name $animate#cancel
5148 * @kind function
5149 * @description Cancels the provided animation.
5150 *
5151 * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
5152 */
5153 cancel: function(runner) {
5154 runner.end && runner.end();
5155 },
41995156
42005157 /**
42015158 *
42025159 * @ngdoc method
42035160 * @name $animate#enter
42045161 * @kind function
4205 * @description Inserts the element into the DOM either after the `after` element or within
4206 * the `parent` element. Once complete, the done() callback will be fired (if provided).
5162 * @description Inserts the element into the DOM either after the `after` element (if provided) or
5163 * as the first child within the `parent` element and then triggers an animation.
5164 * A promise is returned that will be resolved during the next digest once the animation
5165 * has completed.
5166 *
42075167 * @param {DOMElement} element the element which will be inserted into the DOM
42085168 * @param {DOMElement} parent the parent element which will append the element as
4209 * a child (if the after element is not present)
4210 * @param {DOMElement} after the sibling element which will append the element
4211 * after itself
4212 * @param {Function=} done callback function that will be called after the element has been
4213 * inserted into the DOM
5169 * a child (so long as the after element is not present)
5170 * @param {DOMElement=} after the sibling element after which the element will be appended
5171 * @param {object=} options an optional collection of options/styles that will be applied to the element
5172 *
5173 * @return {Promise} the animation callback promise
42145174 */
4215 enter : function(element, parent, after, done) {
4216 if (after) {
4217 after.after(element);
4218 } else {
4219 if (!parent || !parent[0]) {
4220 parent = after.parent();
4221 }
4222 parent.append(element);
4223 }
4224 async(done);
4225 },
4226
4227 /**
4228 *
4229 * @ngdoc method
4230 * @name $animate#leave
4231 * @kind function
4232 * @description Removes the element from the DOM. Once complete, the done() callback will be
4233 * fired (if provided).
4234 * @param {DOMElement} element the element which will be removed from the DOM
4235 * @param {Function=} done callback function that will be called after the element has been
4236 * removed from the DOM
4237 */
4238 leave : function(element, done) {
4239 element.remove();
4240 async(done);
5175 enter: function(element, parent, after, options) {
5176 parent = parent && jqLite(parent);
5177 after = after && jqLite(after);
5178 parent = parent || after.parent();
5179 domInsert(element, parent, after);
5180 return $$animateQueue.push(element, 'enter', prepareAnimateOptions(options));
42415181 },
42425182
42435183 /**
42455185 * @ngdoc method
42465186 * @name $animate#move
42475187 * @kind function
4248 * @description Moves the position of the provided element within the DOM to be placed
4249 * either after the `after` element or inside of the `parent` element. Once complete, the
4250 * done() callback will be fired (if provided).
5188 * @description Inserts (moves) the element into its new position in the DOM either after
5189 * the `after` element (if provided) or as the first child within the `parent` element
5190 * and then triggers an animation. A promise is returned that will be resolved
5191 * during the next digest once the animation has completed.
42515192 *
4252 * @param {DOMElement} element the element which will be moved around within the
4253 * DOM
4254 * @param {DOMElement} parent the parent element where the element will be
4255 * inserted into (if the after element is not present)
4256 * @param {DOMElement} after the sibling element where the element will be
4257 * positioned next to
4258 * @param {Function=} done the callback function (if provided) that will be fired after the
4259 * element has been moved to its new position
5193 * @param {DOMElement} element the element which will be moved into the new DOM position
5194 * @param {DOMElement} parent the parent element which will append the element as
5195 * a child (so long as the after element is not present)
5196 * @param {DOMElement=} after the sibling element after which the element will be appended
5197 * @param {object=} options an optional collection of options/styles that will be applied to the element
5198 *
5199 * @return {Promise} the animation callback promise
42605200 */
4261 move : function(element, parent, after, done) {
4262 // Do not remove element before insert. Removing will cause data associated with the
4263 // element to be dropped. Insert will implicitly do the remove.
4264 this.enter(element, parent, after, done);
5201 move: function(element, parent, after, options) {
5202 parent = parent && jqLite(parent);
5203 after = after && jqLite(after);
5204 parent = parent || after.parent();
5205 domInsert(element, parent, after);
5206 return $$animateQueue.push(element, 'move', prepareAnimateOptions(options));
42655207 },
42665208
42675209 /**
5210 * @ngdoc method
5211 * @name $animate#leave
5212 * @kind function
5213 * @description Triggers an animation and then removes the element from the DOM.
5214 * When the function is called a promise is returned that will be resolved during the next
5215 * digest once the animation has completed.
42685216 *
5217 * @param {DOMElement} element the element which will be removed from the DOM
5218 * @param {object=} options an optional collection of options/styles that will be applied to the element
5219 *
5220 * @return {Promise} the animation callback promise
5221 */
5222 leave: function(element, options) {
5223 return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
5224 element.remove();
5225 });
5226 },
5227
5228 /**
42695229 * @ngdoc method
42705230 * @name $animate#addClass
42715231 * @kind function
4272 * @description Adds the provided className CSS class value to the provided element. Once
4273 * complete, the done() callback will be fired (if provided).
4274 * @param {DOMElement} element the element which will have the className value
4275 * added to it
4276 * @param {string} className the CSS class which will be added to the element
4277 * @param {Function=} done the callback function (if provided) that will be fired after the
4278 * className value has been added to the element
5232 *
5233 * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon
5234 * execution, the addClass operation will only be handled after the next digest and it will not trigger an
5235 * animation if element already contains the CSS class or if the class is removed at a later step.
5236 * Note that class-based animations are treated differently compared to structural animations
5237 * (like enter, move and leave) since the CSS classes may be added/removed at different points
5238 * depending if CSS or JavaScript animations are used.
5239 *
5240 * @param {DOMElement} element the element which the CSS classes will be applied to
5241 * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces)
5242 * @param {object=} options an optional collection of options/styles that will be applied to the element
5243 *
5244 * @return {Promise} the animation callback promise
42795245 */
4280 addClass : function(element, className, done) {
4281 className = isString(className) ?
4282 className :
4283 isArray(className) ? className.join(' ') : '';
4284 forEach(element, function (element) {
4285 jqLiteAddClass(element, className);
4286 });
4287 async(done);
5246 addClass: function(element, className, options) {
5247 options = prepareAnimateOptions(options);
5248 options.addClass = mergeClasses(options.addclass, className);
5249 return $$animateQueue.push(element, 'addClass', options);
42885250 },
42895251
42905252 /**
4291 *
42925253 * @ngdoc method
42935254 * @name $animate#removeClass
42945255 * @kind function
4295 * @description Removes the provided className CSS class value from the provided element.
4296 * Once complete, the done() callback will be fired (if provided).
4297 * @param {DOMElement} element the element which will have the className value
4298 * removed from it
4299 * @param {string} className the CSS class which will be removed from the element
4300 * @param {Function=} done the callback function (if provided) that will be fired after the
4301 * className value has been removed from the element
5256 *
5257 * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon
5258 * execution, the removeClass operation will only be handled after the next digest and it will not trigger an
5259 * animation if element does not contain the CSS class or if the class is added at a later step.
5260 * Note that class-based animations are treated differently compared to structural animations
5261 * (like enter, move and leave) since the CSS classes may be added/removed at different points
5262 * depending if CSS or JavaScript animations are used.
5263 *
5264 * @param {DOMElement} element the element which the CSS classes will be applied to
5265 * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces)
5266 * @param {object=} options an optional collection of options/styles that will be applied to the element
5267 *
5268 * @return {Promise} the animation callback promise
43025269 */
4303 removeClass : function(element, className, done) {
4304 className = isString(className) ?
4305 className :
4306 isArray(className) ? className.join(' ') : '';
4307 forEach(element, function (element) {
4308 jqLiteRemoveClass(element, className);
4309 });
4310 async(done);
5270 removeClass: function(element, className, options) {
5271 options = prepareAnimateOptions(options);
5272 options.removeClass = mergeClasses(options.removeClass, className);
5273 return $$animateQueue.push(element, 'removeClass', options);
43115274 },
43125275
43135276 /**
4314 *
43155277 * @ngdoc method
43165278 * @name $animate#setClass
43175279 * @kind function
4318 * @description Adds and/or removes the given CSS classes to and from the element.
4319 * Once complete, the done() callback will be fired (if provided).
4320 * @param {DOMElement} element the element which will have its CSS classes changed
4321 * removed from it
4322 * @param {string} add the CSS classes which will be added to the element
4323 * @param {string} remove the CSS class which will be removed from the element
4324 * @param {Function=} done the callback function (if provided) that will be fired after the
4325 * CSS classes have been set on the element
5280 *
5281 * @description Performs both the addition and removal of a CSS classes on an element and (during the process)
5282 * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and
5283 * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has
5284 * passed. Note that class-based animations are treated differently compared to structural animations
5285 * (like enter, move and leave) since the CSS classes may be added/removed at different points
5286 * depending if CSS or JavaScript animations are used.
5287 *
5288 * @param {DOMElement} element the element which the CSS classes will be applied to
5289 * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces)
5290 * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces)
5291 * @param {object=} options an optional collection of options/styles that will be applied to the element
5292 *
5293 * @return {Promise} the animation callback promise
43265294 */
4327 setClass : function(element, add, remove, done) {
4328 forEach(element, function (element) {
4329 jqLiteAddClass(element, add);
4330 jqLiteRemoveClass(element, remove);
4331 });
4332 async(done);
5295 setClass: function(element, add, remove, options) {
5296 options = prepareAnimateOptions(options);
5297 options.addClass = mergeClasses(options.addClass, add);
5298 options.removeClass = mergeClasses(options.removeClass, remove);
5299 return $$animateQueue.push(element, 'setClass', options);
43335300 },
43345301
4335 enabled : noop
5302 /**
5303 * @ngdoc method
5304 * @name $animate#animate
5305 * @kind function
5306 *
5307 * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element.
5308 * If any detected CSS transition, keyframe or JavaScript matches the provided className value then the animation will take
5309 * on the provided styles. For example, if a transition animation is set for the given className then the provided from and
5310 * to styles will be applied alongside the given transition. If a JavaScript animation is detected then the provided styles
5311 * will be given in as function paramters into the `animate` method (or as apart of the `options` parameter).
5312 *
5313 * @param {DOMElement} element the element which the CSS styles will be applied to
5314 * @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation.
5315 * @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation.
5316 * @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If
5317 * this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element.
5318 * (Note that if no animation is detected then this value will not be appplied to the element.)
5319 * @param {object=} options an optional collection of options/styles that will be applied to the element
5320 *
5321 * @return {Promise} the animation callback promise
5322 */
5323 animate: function(element, from, to, className, options) {
5324 options = prepareAnimateOptions(options);
5325 options.from = options.from ? extend(options.from, from) : from;
5326 options.to = options.to ? extend(options.to, to) : to;
5327
5328 className = className || 'ng-inline-animate';
5329 options.tempClasses = mergeClasses(options.tempClasses, className);
5330 return $$animateQueue.push(element, 'animate', options);
5331 }
43365332 };
43375333 }];
43385334 }];
43395335
4340 function $$AsyncCallbackProvider(){
5336 function $$AsyncCallbackProvider() {
43415337 this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) {
43425338 return $$rAF.supported
43435339 ? function(fn) { return $$rAF(fn); }
43475343 }];
43485344 }
43495345
5346 /* global stripHash: true */
5347
43505348 /**
43515349 * ! This is a private undocumented service !
43525350 *
43655363 /**
43665364 * @param {object} window The global window object.
43675365 * @param {object} document jQuery wrapped document.
4368 * @param {function()} XHR XMLHttpRequest constructor.
4369 * @param {object} $log console.log or an object with the same interface.
5366 * @param {object} $log window.console or an object with the same interface.
43705367 * @param {object} $sniffer $sniffer service
43715368 */
43725369 function Browser(window, document, $log, $sniffer) {
43975394 } finally {
43985395 outstandingRequestCount--;
43995396 if (outstandingRequestCount === 0) {
4400 while(outstandingRequestCallbacks.length) {
5397 while (outstandingRequestCallbacks.length) {
44015398 try {
44025399 outstandingRequestCallbacks.pop()();
44035400 } catch (e) {
44065403 }
44075404 }
44085405 }
5406 }
5407
5408 function getHash(url) {
5409 var index = url.indexOf('#');
5410 return index === -1 ? '' : url.substr(index);
44095411 }
44105412
44115413 /**
44155417 * @param {function()} callback Function that will be called when no outstanding request
44165418 */
44175419 self.notifyWhenNoOutstandingRequests = function(callback) {
4418 // force browser to execute all pollFns - this is needed so that cookies and other pollers fire
4419 // at some deterministic time in respect to the test runner's actions. Leaving things up to the
4420 // regular poller would result in flaky tests.
4421 forEach(pollFns, function(pollFn){ pollFn(); });
4422
44235420 if (outstandingRequestCount === 0) {
44245421 callback();
44255422 } else {
44285425 };
44295426
44305427 //////////////////////////////////////////////////////////////
4431 // Poll Watcher API
4432 //////////////////////////////////////////////////////////////
4433 var pollFns = [],
4434 pollTimeout;
4435
4436 /**
4437 * @name $browser#addPollFn
4438 *
4439 * @param {function()} fn Poll function to add
4440 *
4441 * @description
4442 * Adds a function to the list of functions that poller periodically executes,
4443 * and starts polling if not started yet.
4444 *
4445 * @returns {function()} the added function
4446 */
4447 self.addPollFn = function(fn) {
4448 if (isUndefined(pollTimeout)) startPoller(100, setTimeout);
4449 pollFns.push(fn);
4450 return fn;
4451 };
4452
4453 /**
4454 * @param {number} interval How often should browser call poll functions (ms)
4455 * @param {function()} setTimeout Reference to a real or fake `setTimeout` function.
4456 *
4457 * @description
4458 * Configures the poller to run in the specified intervals, using the specified
4459 * setTimeout fn and kicks it off.
4460 */
4461 function startPoller(interval, setTimeout) {
4462 (function check() {
4463 forEach(pollFns, function(pollFn){ pollFn(); });
4464 pollTimeout = setTimeout(check, interval);
4465 })();
4466 }
4467
4468 //////////////////////////////////////////////////////////////
44695428 // URL API
44705429 //////////////////////////////////////////////////////////////
44715430
4472 var lastBrowserUrl = location.href,
5431 var cachedState, lastHistoryState,
5432 lastBrowserUrl = location.href,
44735433 baseElement = document.find('base'),
4474 newLocation = null;
5434 reloadLocation = null;
5435
5436 cacheState();
5437 lastHistoryState = cachedState;
44755438
44765439 /**
44775440 * @name $browser#url
44905453 * {@link ng.$location $location service} to change url.
44915454 *
44925455 * @param {string} url New url (when used as setter)
4493 * @param {boolean=} replace Should new url replace current history record ?
5456 * @param {boolean=} replace Should new url replace current history record?
5457 * @param {object=} state object to use with pushState/replaceState
44945458 */
4495 self.url = function(url, replace) {
5459 self.url = function(url, replace, state) {
5460 // In modern browsers `history.state` is `null` by default; treating it separately
5461 // from `undefined` would cause `$browser.url('/foo')` to change `history.state`
5462 // to undefined via `pushState`. Instead, let's change `undefined` to `null` here.
5463 if (isUndefined(state)) {
5464 state = null;
5465 }
5466
44965467 // Android Browser BFCache causes location, history reference to become stale.
44975468 if (location !== window.location) location = window.location;
44985469 if (history !== window.history) history = window.history;
44995470
45005471 // setter
45015472 if (url) {
4502 if (lastBrowserUrl == url) return;
5473 var sameState = lastHistoryState === state;
5474
5475 // Don't change anything if previous and current URLs and states match. This also prevents
5476 // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
5477 // See https://github.com/angular/angular.js/commit/ffb2701
5478 if (lastBrowserUrl === url && (!$sniffer.history || sameState)) {
5479 return self;
5480 }
5481 var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
45035482 lastBrowserUrl = url;
4504 if ($sniffer.history) {
4505 if (replace) history.replaceState(null, '', url);
4506 else {
4507 history.pushState(null, '', url);
4508 // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462
4509 baseElement.attr('href', baseElement.attr('href'));
5483 lastHistoryState = state;
5484 // Don't use history API if only the hash changed
5485 // due to a bug in IE10/IE11 which leads
5486 // to not firing a `hashchange` nor `popstate` event
5487 // in some cases (see #9143).
5488 if ($sniffer.history && (!sameBase || !sameState)) {
5489 history[replace ? 'replaceState' : 'pushState'](state, '', url);
5490 cacheState();
5491 // Do the assignment again so that those two variables are referentially identical.
5492 lastHistoryState = cachedState;
5493 } else {
5494 if (!sameBase || reloadLocation) {
5495 reloadLocation = url;
45105496 }
4511 } else {
4512 newLocation = url;
45135497 if (replace) {
45145498 location.replace(url);
5499 } else if (!sameBase) {
5500 location.href = url;
45155501 } else {
4516 location.href = url;
5502 location.hash = getHash(url);
45175503 }
45185504 }
45195505 return self;
45205506 // getter
45215507 } else {
4522 // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href
4523 // methods not updating location.href synchronously.
5508 // - reloadLocation is needed as browsers don't allow to read out
5509 // the new location.href if a reload happened.
45245510 // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
4525 return newLocation || location.href.replace(/%27/g,"'");
4526 }
5511 return reloadLocation || location.href.replace(/%27/g,"'");
5512 }
5513 };
5514
5515 /**
5516 * @name $browser#state
5517 *
5518 * @description
5519 * This method is a getter.
5520 *
5521 * Return history.state or null if history.state is undefined.
5522 *
5523 * @returns {object} state
5524 */
5525 self.state = function() {
5526 return cachedState;
45275527 };
45285528
45295529 var urlChangeListeners = [],
45305530 urlChangeInit = false;
45315531
5532 function cacheStateAndFireUrlChange() {
5533 cacheState();
5534 fireUrlChange();
5535 }
5536
5537 function getCurrentState() {
5538 try {
5539 return history.state;
5540 } catch (e) {
5541 // MSIE can reportedly throw when there is no state (UNCONFIRMED).
5542 }
5543 }
5544
5545 // This variable should be used *only* inside the cacheState function.
5546 var lastCachedState = null;
5547 function cacheState() {
5548 // This should be the only place in $browser where `history.state` is read.
5549 cachedState = getCurrentState();
5550 cachedState = isUndefined(cachedState) ? null : cachedState;
5551
5552 // Prevent callbacks fo fire twice if both hashchange & popstate were fired.
5553 if (equals(cachedState, lastCachedState)) {
5554 cachedState = lastCachedState;
5555 }
5556 lastCachedState = cachedState;
5557 }
5558
45325559 function fireUrlChange() {
4533 newLocation = null;
4534 if (lastBrowserUrl == self.url()) return;
5560 if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
5561 return;
5562 }
45355563
45365564 lastBrowserUrl = self.url();
5565 lastHistoryState = cachedState;
45375566 forEach(urlChangeListeners, function(listener) {
4538 listener(self.url());
5567 listener(self.url(), cachedState);
45395568 });
45405569 }
45415570
45685597 // changed by push/replaceState
45695598
45705599 // html5 history api - popstate event
4571 if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange);
5600 if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange);
45725601 // hashchange event
4573 if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange);
4574 // polling
4575 else self.addPollFn(fireUrlChange);
5602 jqLite(window).on('hashchange', cacheStateAndFireUrlChange);
45765603
45775604 urlChangeInit = true;
45785605 }
45805607 urlChangeListeners.push(callback);
45815608 return callback;
45825609 };
5610
5611 /**
5612 * @private
5613 * Remove popstate and hashchange handler from window.
5614 *
5615 * NOTE: this api is intended for use only by $rootScope.
5616 */
5617 self.$$applicationDestroyed = function() {
5618 jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange);
5619 };
5620
5621 /**
5622 * Checks whether the url has changed outside of Angular.
5623 * Needs to be exported to be able to check for changes that have been done in sync,
5624 * as hashchange/popstate events fire in async.
5625 */
5626 self.$$checkUrlChange = fireUrlChange;
45835627
45845628 //////////////////////////////////////////////////////////////
45855629 // Misc API
45985642 var href = baseElement.attr('href');
45995643 return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : '';
46005644 };
4601
4602 //////////////////////////////////////////////////////////////
4603 // Cookies API
4604 //////////////////////////////////////////////////////////////
4605 var lastCookies = {};
4606 var lastCookieString = '';
4607 var cookiePath = self.baseHref();
4608
4609 /**
4610 * @name $browser#cookies
4611 *
4612 * @param {string=} name Cookie name
4613 * @param {string=} value Cookie value
4614 *
4615 * @description
4616 * The cookies method provides a 'private' low level access to browser cookies.
4617 * It is not meant to be used directly, use the $cookie service instead.
4618 *
4619 * The return values vary depending on the arguments that the method was called with as follows:
4620 *
4621 * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify
4622 * it
4623 * - cookies(name, value) -> set name to value, if value is undefined delete the cookie
4624 * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that
4625 * way)
4626 *
4627 * @returns {Object} Hash of all cookies (if called without any parameter)
4628 */
4629 self.cookies = function(name, value) {
4630 /* global escape: false, unescape: false */
4631 var cookieLength, cookieArray, cookie, i, index;
4632
4633 if (name) {
4634 if (value === undefined) {
4635 rawDocument.cookie = escape(name) + "=;path=" + cookiePath +
4636 ";expires=Thu, 01 Jan 1970 00:00:00 GMT";
4637 } else {
4638 if (isString(value)) {
4639 cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) +
4640 ';path=' + cookiePath).length + 1;
4641
4642 // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
4643 // - 300 cookies
4644 // - 20 cookies per unique domain
4645 // - 4096 bytes per cookie
4646 if (cookieLength > 4096) {
4647 $log.warn("Cookie '"+ name +
4648 "' possibly not set or overflowed because it was too large ("+
4649 cookieLength + " > 4096 bytes)!");
4650 }
4651 }
4652 }
4653 } else {
4654 if (rawDocument.cookie !== lastCookieString) {
4655 lastCookieString = rawDocument.cookie;
4656 cookieArray = lastCookieString.split("; ");
4657 lastCookies = {};
4658
4659 for (i = 0; i < cookieArray.length; i++) {
4660 cookie = cookieArray[i];
4661 index = cookie.indexOf('=');
4662 if (index > 0) { //ignore nameless cookies
4663 name = unescape(cookie.substring(0, index));
4664 // the first value that is seen for a cookie is the most
4665 // specific one. values for the same cookie name that
4666 // follow are for less specific paths.
4667 if (lastCookies[name] === undefined) {
4668 lastCookies[name] = unescape(cookie.substring(index + 1));
4669 }
4670 }
4671 }
4672 }
4673 return lastCookies;
4674 }
4675 };
4676
46775645
46785646 /**
46795647 * @name $browser#defer
47235691
47245692 }
47255693
4726 function $BrowserProvider(){
5694 function $BrowserProvider() {
47275695 this.$get = ['$window', '$log', '$sniffer', '$document',
4728 function( $window, $log, $sniffer, $document){
5696 function($window, $log, $sniffer, $document) {
47295697 return new Browser($window, $document, $log, $sniffer);
47305698 }];
47315699 }
48895857 * @returns {*} the value stored.
48905858 */
48915859 put: function(key, value) {
5860 if (isUndefined(value)) return;
48925861 if (capacity < Number.MAX_VALUE) {
48935862 var lruEntry = lruHash[key] || (lruHash[key] = {key: key});
48945863
48955864 refresh(lruEntry);
48965865 }
48975866
4898 if (isUndefined(value)) return;
48995867 if (!(key in data)) size++;
49005868 data[key] = value;
49015869
50996067 * ```
51006068 *
51016069 * **Note:** the `script` tag containing the template does not need to be included in the `head` of
5102 * the document, but it must be below the `ng-app` definition.
5103 *
5104 * Adding via the $templateCache service:
6070 * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE,
6071 * element with ng-app attribute), otherwise the template will be ignored.
6072 *
6073 * Adding via the `$templateCache` service:
51056074 *
51066075 * ```js
51076076 * var myApp = angular.module('myApp', []);
51286097 return $cacheFactory('templates');
51296098 }];
51306099 }
6100
6101 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
6102 * Any commits to this file should be reviewed with security in mind. *
6103 * Changes to this file can potentially create security vulnerabilities. *
6104 * An approval from 2 Core members with history of modifying *
6105 * this file is required. *
6106 * *
6107 * Does the change somehow allow for arbitrary javascript to be executed? *
6108 * Or allows for someone to change the prototype of built-in objects? *
6109 * Or gives undesired access to variables likes document or window? *
6110 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
51316111
51326112 /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
51336113 *
51906170 * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
51916171 * transclude: false,
51926172 * restrict: 'A',
6173 * templateNamespace: 'html',
51936174 * scope: false,
51946175 * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
5195 * controllerAs: 'stringAlias',
6176 * controllerAs: 'stringIdentifier',
6177 * bindToController: false,
51966178 * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
51976179 * compile: function compile(tElement, tAttrs, transclude) {
51986180 * return {
52406222 * The directive definition object provides instructions to the {@link ng.$compile
52416223 * compiler}. The attributes are:
52426224 *
6225 * #### `multiElement`
6226 * When this property is set to true, the HTML compiler will collect DOM nodes between
6227 * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
6228 * together as the directive elements. It is recommended that this feature be used on directives
6229 * which are not strictly behavioural (such as {@link ngClick}), and which
6230 * do not manipulate or replace child nodes (such as {@link ngInclude}).
6231 *
52436232 * #### `priority`
52446233 * When there are multiple directives defined on a single DOM element, sometimes it
52456234 * is necessary to specify the order in which the directives are applied. The `priority` is used
52516240 * #### `terminal`
52526241 * If set to true then the current `priority` will be the last set of directives
52536242 * which will execute (any directives at the current priority will still execute
5254 * as the order of execution on same `priority` is undefined).
6243 * as the order of execution on same `priority` is undefined). Note that expressions
6244 * and other directives used in the directive's template will also be excluded from execution.
52556245 *
52566246 * #### `scope`
52576247 * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the
52846274 * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
52856275 * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent
52866276 * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You
5287 * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional.
6277 * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If
6278 * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use
6279 * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional).
52886280 *
52896281 * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
52906282 * If no `attr` name is specified then the attribute name is assumed to be the same as the
52976289 * by calling the `localFn` as `localFn({amount: 22})`.
52986290 *
52996291 *
6292 * #### `bindToController`
6293 * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will
6294 * allow a component to have its properties bound to the controller, rather than to scope. When the controller
6295 * is instantiated, the initial values of the isolate scope bindings are already available.
53006296 *
53016297 * #### `controller`
53026298 * Controller constructor function. The controller is instantiated before the
53076303 * * `$scope` - Current scope associated with the element
53086304 * * `$element` - Current element
53096305 * * `$attrs` - Current attributes object for the element
5310 * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope.
5311 * The scope can be overridden by an optional first argument.
5312 * `function([scope], cloneLinkingFn)`.
6306 * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
6307 * `function([scope], cloneLinkingFn, futureParentElement)`.
6308 * * `scope`: optional argument to override the scope.
6309 * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content.
6310 * * `futureParentElement`:
6311 * * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
6312 * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
6313 * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
6314 * and when the `cloneLinkinFn` is passed,
6315 * as those elements need to created and cloned in a special way when they are defined outside their
6316 * usual containers (e.g. like `<svg>`).
6317 * * See also the `directive.templateNamespace` property.
53136318 *
53146319 *
53156320 * #### `require`
53166321 * Require another directive and inject its controller as the fourth argument to the linking function. The
53176322 * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the
53186323 * injected argument will be an array in corresponding order. If no such directive can be
5319 * found, or if the directive does not have a controller, then an error is raised. The name can be prefixed with:
6324 * found, or if the directive does not have a controller, then an error is raised (unless no link function
6325 * is specified, in which case error checking is skipped). The name can be prefixed with:
53206326 *
53216327 * * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
53226328 * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
53236329 * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found.
6330 * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found.
53246331 * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass
53256332 * `null` to the `link` fn if not found.
6333 * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass
6334 * `null` to the `link` fn if not found.
53266335 *
53276336 *
53286337 * #### `controllerAs`
5329 * Controller alias at the directive scope. An alias for the controller so it
5330 * can be referenced at the directive template. The directive needs to define a scope for this
5331 * configuration to be used. Useful in the case when directive is used as component.
6338 * Identifier name for a reference to the controller in the directive's scope.
6339 * This allows the controller to be referenced from the directive template. The directive
6340 * needs to define a scope for this configuration to be used. Useful in the case when
6341 * directive is used as component.
53326342 *
53336343 *
53346344 * #### `restrict`
53356345 * String of subset of `EACM` which restricts the directive to a specific directive
5336 * declaration style. If omitted, the default (attributes only) is used.
5337 *
5338 * * `E` - Element name: `<my-directive></my-directive>`
6346 * declaration style. If omitted, the defaults (elements and attributes) are used.
6347 *
6348 * * `E` - Element name (default): `<my-directive></my-directive>`
53396349 * * `A` - Attribute (default): `<div my-directive="exp"></div>`
53406350 * * `C` - Class: `<div class="my-directive: exp;"></div>`
53416351 * * `M` - Comment: `<!-- directive: my-directive exp -->`
53426352 *
6353 *
6354 * #### `templateNamespace`
6355 * String representing the document type used by the markup in the template.
6356 * AngularJS needs this information as those elements need to be created and cloned
6357 * in a special way when they are defined outside their usual containers like `<svg>` and `<math>`.
6358 *
6359 * * `html` - All root nodes in the template are HTML. Root nodes may also be
6360 * top-level elements such as `<svg>` or `<math>`.
6361 * * `svg` - The root nodes in the template are SVG elements (excluding `<math>`).
6362 * * `math` - The root nodes in the template are MathML elements (excluding `<svg>`).
6363 *
6364 * If no `templateNamespace` is specified, then the namespace is considered to be `html`.
53436365 *
53446366 * #### `template`
53456367 * HTML markup that may:
53556377 *
53566378 *
53576379 * #### `templateUrl`
5358 * Same as `template` but the template is loaded from the specified URL. Because
5359 * the template loading is asynchronous the compilation/linking is suspended until the template
5360 * is loaded.
6380 * This is similar to `template` but the template is loaded from the specified URL, asynchronously.
6381 *
6382 * Because template loading is asynchronous the compiler will suspend compilation of directives on that element
6383 * for later when the template has been resolved. In the meantime it will continue to compile and link
6384 * sibling and parent elements as though this element had not contained any directives.
6385 *
6386 * The compiler does not suspend the entire compilation to wait for templates to be loaded because this
6387 * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the
6388 * case when only one deeply nested directive has `templateUrl`.
6389 *
6390 * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache}
53616391 *
53626392 * You can specify `templateUrl` as a string representing the URL or as a function which takes two
53636393 * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns
53646394 * a string value representing the url. In either case, the template URL is passed through {@link
5365 * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}.
5366 *
5367 *
5368 * #### `replace` ([*DEPRECATED*!], will be removed in next major release)
6395 * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}.
6396 *
6397 *
6398 * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0)
53696399 * specify what the template should replace. Defaults to `false`.
53706400 *
53716401 * * `true` - the template will replace the directive's element.
53726402 * * `false` - the template will replace the contents of the directive's element.
53736403 *
53746404 * The replacement process migrates all of the attributes / classes from the old element to the new
5375 * one. See the {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive
6405 * one. See the {@link guide/directive#template-expanding-directive
53766406 * Directives Guide} for an example.
53776407 *
6408 * There are very few scenarios where element replacement is required for the application function,
6409 * the main one being reusable custom components that are used within SVG contexts
6410 * (because SVG doesn't work with custom elements in the DOM tree).
6411 *
53786412 * #### `transclude`
5379 * compile the content of the element and make it available to the directive.
5380 * Typically used with {@link ng.directive:ngTransclude
5381 * ngTransclude}. The advantage of transclusion is that the linking function receives a
5382 * transclusion function which is pre-bound to the correct scope. In a typical setup the widget
5383 * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate`
5384 * scope. This makes it possible for the widget to have private state, and the transclusion to
5385 * be bound to the parent (pre-`isolate`) scope.
5386 *
5387 * * `true` - transclude the content of the directive.
5388 * * `'element'` - transclude the whole element including any directives defined at lower priority.
5389 *
5390 * <div class="alert alert-warning">
5391 * **Note:** When testing an element transclude directive you must not place the directive at the root of the
5392 * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives
5393 * Testing Transclusion Directives}.
5394 * </div>
6413 * Extract the contents of the element where the directive appears and make it available to the directive.
6414 * The contents are compiled and provided to the directive as a **transclusion function**. See the
6415 * {@link $compile#transclusion Transclusion} section below.
6416 *
6417 * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
6418 * directive's element or the entire element:
6419 *
6420 * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
6421 * * `'element'` - transclude the whole of the directive's element including any directives on this
6422 * element that defined at a lower priority than this directive. When used, the `template`
6423 * property is ignored.
6424 *
53956425 *
53966426 * #### `compile`
53976427 *
54276457 * `templateUrl` declaration or manual compilation inside the compile function.
54286458 * </div>
54296459 *
5430 * <div class="alert alert-error">
6460 * <div class="alert alert-danger">
54316461 * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it
54326462 * e.g. does not know about the right outer scope. Please use the transclude function that is passed
54336463 * to the link function instead.
54646494 * * `iAttrs` - instance attributes - Normalized list of attributes declared on this element shared
54656495 * between all directive linking functions.
54666496 *
5467 * * `controller` - a controller instance - A controller instance if at least one directive on the
5468 * element defines a controller. The controller is shared among all the directives, which allows
5469 * the directives to use the controllers as a communication channel.
6497 * * `controller` - the directive's required controller instance(s) - Instances are shared
6498 * among all directives, which allows the directives to use the controllers as a communication
6499 * channel. The exact value depends on the directive's `require` property:
6500 * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one
6501 * * `string`: the controller instance
6502 * * `array`: array of controller instances
6503 *
6504 * If a required controller cannot be found, and it is optional, the instance is `null`,
6505 * otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown.
6506 *
6507 * Note that you can also require the directive's own controller - it will be made available like
6508 * like any other controller.
54706509 *
54716510 * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
5472 * The scope can be overridden by an optional first argument. This is the same as the `$transclude`
5473 * parameter of directive controllers.
5474 * `function([scope], cloneLinkingFn)`.
5475 *
6511 * This is the same as the `$transclude`
6512 * parameter of directive controllers, see there for details.
6513 * `function([scope], cloneLinkingFn, futureParentElement)`.
54766514 *
54776515 * #### Pre-linking function
54786516 *
54816519 *
54826520 * #### Post-linking function
54836521 *
5484 * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function.
5485 *
5486 * <a name="Attributes"></a>
6522 * Executed after the child elements are linked.
6523 *
6524 * Note that child elements that contain `templateUrl` directives will not have been compiled
6525 * and linked since they are waiting for their template to load asynchronously and their own
6526 * compilation and linking has been suspended until that occurs.
6527 *
6528 * It is safe to do DOM transformation in the post-linking function on elements that are not waiting
6529 * for their async templates to be resolved.
6530 *
6531 *
6532 * ### Transclusion
6533 *
6534 * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and
6535 * copying them to another part of the DOM, while maintaining their connection to the original AngularJS
6536 * scope from where they were taken.
6537 *
6538 * Transclusion is used (often with {@link ngTransclude}) to insert the
6539 * original contents of a directive's element into a specified place in the template of the directive.
6540 * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded
6541 * content has access to the properties on the scope from which it was taken, even if the directive
6542 * has isolated scope.
6543 * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}.
6544 *
6545 * This makes it possible for the widget to have private state for its template, while the transcluded
6546 * content has access to its originating scope.
6547 *
6548 * <div class="alert alert-warning">
6549 * **Note:** When testing an element transclude directive you must not place the directive at the root of the
6550 * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives
6551 * Testing Transclusion Directives}.
6552 * </div>
6553 *
6554 * #### Transclusion Functions
6555 *
6556 * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion
6557 * function** to the directive's `link` function and `controller`. This transclusion function is a special
6558 * **linking function** that will return the compiled contents linked to a new transclusion scope.
6559 *
6560 * <div class="alert alert-info">
6561 * If you are just using {@link ngTransclude} then you don't need to worry about this function, since
6562 * ngTransclude will deal with it for us.
6563 * </div>
6564 *
6565 * If you want to manually control the insertion and removal of the transcluded content in your directive
6566 * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery
6567 * object that contains the compiled DOM, which is linked to the correct transclusion scope.
6568 *
6569 * When you call a transclusion function you can pass in a **clone attach function**. This function accepts
6570 * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded
6571 * content and the `scope` is the newly created transclusion scope, to which the clone is bound.
6572 *
6573 * <div class="alert alert-info">
6574 * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function
6575 * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope.
6576 * </div>
6577 *
6578 * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone
6579 * attach function**:
6580 *
6581 * ```js
6582 * var transcludedContent, transclusionScope;
6583 *
6584 * $transclude(function(clone, scope) {
6585 * element.append(clone);
6586 * transcludedContent = clone;
6587 * transclusionScope = scope;
6588 * });
6589 * ```
6590 *
6591 * Later, if you want to remove the transcluded content from your DOM then you should also destroy the
6592 * associated transclusion scope:
6593 *
6594 * ```js
6595 * transcludedContent.remove();
6596 * transclusionScope.$destroy();
6597 * ```
6598 *
6599 * <div class="alert alert-info">
6600 * **Best Practice**: if you intend to add and remove transcluded content manually in your directive
6601 * (by calling the transclude function to get the DOM and calling `element.remove()` to remove it),
6602 * then you are also responsible for calling `$destroy` on the transclusion scope.
6603 * </div>
6604 *
6605 * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat}
6606 * automatically destroy their transluded clones as necessary so you do not need to worry about this if
6607 * you are simply using {@link ngTransclude} to inject the transclusion into your directive.
6608 *
6609 *
6610 * #### Transclusion Scopes
6611 *
6612 * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion
6613 * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed
6614 * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it
6615 * was taken.
6616 *
6617 * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look
6618 * like this:
6619 *
6620 * ```html
6621 * <div ng-app>
6622 * <div isolate>
6623 * <div transclusion>
6624 * </div>
6625 * </div>
6626 * </div>
6627 * ```
6628 *
6629 * The `$parent` scope hierarchy will look like this:
6630 *
6631 * ```
6632 * - $rootScope
6633 * - isolate
6634 * - transclusion
6635 * ```
6636 *
6637 * but the scopes will inherit prototypically from different scopes to their `$parent`.
6638 *
6639 * ```
6640 * - $rootScope
6641 * - transclusion
6642 * - isolate
6643 * ```
6644 *
6645 *
54876646 * ### Attributes
54886647 *
54896648 * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the
55216680 * }
55226681 * ```
55236682 *
5524 * Below is an example using `$compileProvider`.
6683 * ## Example
55256684 *
55266685 * <div class="alert alert-warning">
55276686 * **Note**: Typically directives are registered with `module.directive`. The example below is
55636722 }]);
55646723 </script>
55656724 <div ng-controller="GreeterController">
5566 <input ng-model="name"> <br>
5567 <textarea ng-model="html"></textarea> <br>
6725 <input ng-model="name"> <br/>
6726 <textarea ng-model="html"></textarea> <br/>
55686727 <div compile="html"></div>
55696728 </div>
55706729 </file>
55846743 *
55856744 *
55866745 * @param {string|DOMElement} element Element or HTML string to compile into a template function.
5587 * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives.
6746 * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED.
6747 *
6748 * <div class="alert alert-danger">
6749 * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it
6750 * e.g. will not use the right outer scope. Please pass the transclude function as a
6751 * `parentBoundTranscludeFn` to the link function instead.
6752 * </div>
6753 *
55886754 * @param {number} maxPriority only apply directives lower than given priority (Only effects the
55896755 * root element(s), not their children)
5590 * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template
6756 * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template
55916757 * (a DOM element/tree) to a scope. Where:
55926758 *
55936759 * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
55946760 * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
55956761 * `template` and call the `cloneAttachFn` function allowing the caller to attach the
55966762 * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
5597 * called as: <br> `cloneAttachFn(clonedElement, scope)` where:
6763 * called as: <br/> `cloneAttachFn(clonedElement, scope)` where:
55986764 *
55996765 * * `clonedElement` - is a clone of the original `element` passed into the compiler.
56006766 * * `scope` - is the current scope with which the linking function is working with.
6767 *
6768 * * `options` - An optional object hash with linking options. If `options` is provided, then the following
6769 * keys may be used to control linking behavior:
6770 *
6771 * * `parentBoundTranscludeFn` - the transclude function made available to
6772 * directives; if given, it will be passed through to the link functions of
6773 * directives found in `element` during compilation.
6774 * * `transcludeControllers` - an object hash with keys that map controller names
6775 * to controller instances; if given, it will make the controllers
6776 * available to directives.
6777 * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add
6778 * the cloned elements; only needed for transcludes that are allowed to contain non html
6779 * elements (e.g. SVG elements). See also the directive.controller property.
56016780 *
56026781 * Calling the linking function returns the element of the template. It is either the original
56036782 * element passed in, or the clone of the element if the `cloneAttachFn` is provided.
56376816 /**
56386817 * @ngdoc provider
56396818 * @name $compileProvider
5640 * @kind function
56416819 *
56426820 * @description
56436821 */
56456823 function $CompileProvider($provide, $$sanitizeUriProvider) {
56466824 var hasDirectives = {},
56476825 Suffix = 'Directive',
5648 COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/,
5649 CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/;
6826 COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/,
6827 CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/,
6828 ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'),
6829 REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/;
56506830
56516831 // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes
56526832 // The assumption is that future DOM event attribute names will begin with
56536833 // 'on' and be composed of only English letters.
56546834 var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/;
6835
6836 function parseIsolateBindings(scope, directiveName, isController) {
6837 var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/;
6838
6839 var bindings = {};
6840
6841 forEach(scope, function(definition, scopeName) {
6842 var match = definition.match(LOCAL_REGEXP);
6843
6844 if (!match) {
6845 throw $compileMinErr('iscp',
6846 "Invalid {3} for directive '{0}'." +
6847 " Definition: {... {1}: '{2}' ...}",
6848 directiveName, scopeName, definition,
6849 (isController ? "controller bindings definition" :
6850 "isolate scope definition"));
6851 }
6852
6853 bindings[scopeName] = {
6854 mode: match[1][0],
6855 collection: match[2] === '*',
6856 optional: match[3] === '?',
6857 attrName: match[4] || scopeName
6858 };
6859 });
6860
6861 return bindings;
6862 }
6863
6864 function parseDirectiveBindings(directive, directiveName) {
6865 var bindings = {
6866 isolateScope: null,
6867 bindToController: null
6868 };
6869 if (isObject(directive.scope)) {
6870 if (directive.bindToController === true) {
6871 bindings.bindToController = parseIsolateBindings(directive.scope,
6872 directiveName, true);
6873 bindings.isolateScope = {};
6874 } else {
6875 bindings.isolateScope = parseIsolateBindings(directive.scope,
6876 directiveName, false);
6877 }
6878 }
6879 if (isObject(directive.bindToController)) {
6880 bindings.bindToController =
6881 parseIsolateBindings(directive.bindToController, directiveName, true);
6882 }
6883 if (isObject(bindings.bindToController)) {
6884 var controller = directive.controller;
6885 var controllerAs = directive.controllerAs;
6886 if (!controller) {
6887 // There is no controller, there may or may not be a controllerAs property
6888 throw $compileMinErr('noctrl',
6889 "Cannot bind to controller without directive '{0}'s controller.",
6890 directiveName);
6891 } else if (!identifierForController(controller, controllerAs)) {
6892 // There is a controller, but no identifier or controllerAs property
6893 throw $compileMinErr('noident',
6894 "Cannot bind to controller without identifier for directive '{0}'.",
6895 directiveName);
6896 }
6897 }
6898 return bindings;
6899 }
6900
6901 function assertValidDirectiveName(name) {
6902 var letter = name.charAt(0);
6903 if (!letter || letter !== lowercase(letter)) {
6904 throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name);
6905 }
6906 if (name !== name.trim()) {
6907 throw $compileMinErr('baddir',
6908 "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
6909 name);
6910 }
6911 }
56556912
56566913 /**
56576914 * @ngdoc method
56716928 this.directive = function registerDirective(name, directiveFactory) {
56726929 assertNotHasOwnProperty(name, 'directive');
56736930 if (isString(name)) {
6931 assertValidDirectiveName(name);
56746932 assertArg(directiveFactory, 'directiveFactory');
56756933 if (!hasDirectives.hasOwnProperty(name)) {
56766934 hasDirectives[name] = [];
56896947 directive.index = index;
56906948 directive.name = directive.name || name;
56916949 directive.require = directive.require || (directive.controller && directive.name);
5692 directive.restrict = directive.restrict || 'A';
6950 directive.restrict = directive.restrict || 'EA';
6951 var bindings = directive.$$bindings =
6952 parseDirectiveBindings(directive, directive.name);
6953 if (isObject(bindings.isolateScope)) {
6954 directive.$$isolateBindings = bindings.isolateScope;
6955 }
6956 directive.$$moduleName = directiveFactory.$$moduleName;
56936957 directives.push(directive);
56946958 } catch (e) {
56956959 $exceptionHandler(e);
57156979 * Retrieves or overrides the default regular expression that is used for whitelisting of safe
57166980 * urls during a[href] sanitization.
57176981 *
5718 * The sanitization is a security measure aimed at prevent XSS attacks via html links.
6982 * The sanitization is a security measure aimed at preventing XSS attacks via html links.
57196983 *
57206984 * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
57216985 * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
57657029 }
57667030 };
57677031
7032 /**
7033 * @ngdoc method
7034 * @name $compileProvider#debugInfoEnabled
7035 *
7036 * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the
7037 * current debugInfoEnabled state
7038 * @returns {*} current value if used as getter or itself (chaining) if used as setter
7039 *
7040 * @kind function
7041 *
7042 * @description
7043 * Call this method to enable/disable various debug runtime information in the compiler such as adding
7044 * binding information and a reference to the current scope on to DOM elements.
7045 * If enabled, the compiler will add the following to DOM elements that have been bound to the scope
7046 * * `ng-binding` CSS class
7047 * * `$binding` data property containing an array of the binding expressions
7048 *
7049 * You may want to disable this in production for a significant performance boost. See
7050 * {@link guide/production#disabling-debug-data Disabling Debug Data} for more.
7051 *
7052 * The default value is true.
7053 */
7054 var debugInfoEnabled = true;
7055 this.debugInfoEnabled = function(enabled) {
7056 if (isDefined(enabled)) {
7057 debugInfoEnabled = enabled;
7058 return this;
7059 }
7060 return debugInfoEnabled;
7061 };
7062
57687063 this.$get = [
5769 '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
7064 '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
57707065 '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri',
5771 function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
7066 function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
57727067 $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {
57737068
5774 var Attributes = function(element, attr) {
7069 var Attributes = function(element, attributesToCopy) {
7070 if (attributesToCopy) {
7071 var keys = Object.keys(attributesToCopy);
7072 var i, l, key;
7073
7074 for (i = 0, l = keys.length; i < l; i++) {
7075 key = keys[i];
7076 this[key] = attributesToCopy[key];
7077 }
7078 } else {
7079 this.$attr = {};
7080 }
7081
57757082 this.$$element = element;
5776 this.$attr = attr || {};
57777083 };
57787084
57797085 Attributes.prototype = {
7086 /**
7087 * @ngdoc method
7088 * @name $compile.directive.Attributes#$normalize
7089 * @kind function
7090 *
7091 * @description
7092 * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or
7093 * `data-`) to its normalized, camelCase form.
7094 *
7095 * Also there is special case for Moz prefix starting with upper case letter.
7096 *
7097 * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
7098 *
7099 * @param {string} name Name to normalize
7100 */
57807101 $normalize: directiveNormalize,
57817102
57827103
57917112 *
57927113 * @param {string} classVal The className value that will be added to the element
57937114 */
5794 $addClass : function(classVal) {
5795 if(classVal && classVal.length > 0) {
7115 $addClass: function(classVal) {
7116 if (classVal && classVal.length > 0) {
57967117 $animate.addClass(this.$$element, classVal);
57977118 }
57987119 },
58087129 *
58097130 * @param {string} classVal The className value that will be removed from the element
58107131 */
5811 $removeClass : function(classVal) {
5812 if(classVal && classVal.length > 0) {
7132 $removeClass: function(classVal) {
7133 if (classVal && classVal.length > 0) {
58137134 $animate.removeClass(this.$$element, classVal);
58147135 }
58157136 },
58267147 * @param {string} newClasses The current CSS className value
58277148 * @param {string} oldClasses The former CSS className value
58287149 */
5829 $updateClass : function(newClasses, oldClasses) {
7150 $updateClass: function(newClasses, oldClasses) {
58307151 var toAdd = tokenDifference(newClasses, oldClasses);
7152 if (toAdd && toAdd.length) {
7153 $animate.addClass(this.$$element, toAdd);
7154 }
7155
58317156 var toRemove = tokenDifference(oldClasses, newClasses);
5832
5833 if(toAdd.length === 0) {
7157 if (toRemove && toRemove.length) {
58347158 $animate.removeClass(this.$$element, toRemove);
5835 } else if(toRemove.length === 0) {
5836 $animate.addClass(this.$$element, toAdd);
5837 } else {
5838 $animate.setClass(this.$$element, toAdd, toRemove);
58397159 }
58407160 },
58417161
58537173 //is set through this function since it may cause $updateClass to
58547174 //become unstable.
58557175
5856 var booleanKey = getBooleanAttrName(this.$$element[0], key),
5857 normalizedVal,
7176 var node = this.$$element[0],
7177 booleanKey = getBooleanAttrName(node, key),
7178 aliasedKey = getAliasedAttrName(node, key),
7179 observer = key,
58587180 nodeName;
58597181
58607182 if (booleanKey) {
58617183 this.$$element.prop(key, value);
58627184 attrName = booleanKey;
7185 } else if (aliasedKey) {
7186 this[aliasedKey] = value;
7187 observer = aliasedKey;
58637188 }
58647189
58657190 this[key] = value;
58767201
58777202 nodeName = nodeName_(this.$$element);
58787203
5879 // sanitize a[href] and img[src] values
5880 if ((nodeName === 'A' && key === 'href') ||
5881 (nodeName === 'IMG' && key === 'src')) {
7204 if ((nodeName === 'a' && key === 'href') ||
7205 (nodeName === 'img' && key === 'src')) {
7206 // sanitize a[href] and img[src] values
58827207 this[key] = value = $$sanitizeUri(value, key === 'src');
7208 } else if (nodeName === 'img' && key === 'srcset') {
7209 // sanitize img[srcset] values
7210 var result = "";
7211
7212 // first check if there are spaces because it's not the same pattern
7213 var trimmedSrcset = trim(value);
7214 // ( 999x ,| 999w ,| ,|, )
7215 var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/;
7216 var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/;
7217
7218 // split srcset into tuple of uri and descriptor except for the last item
7219 var rawUris = trimmedSrcset.split(pattern);
7220
7221 // for each tuples
7222 var nbrUrisWith2parts = Math.floor(rawUris.length / 2);
7223 for (var i = 0; i < nbrUrisWith2parts; i++) {
7224 var innerIdx = i * 2;
7225 // sanitize the uri
7226 result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
7227 // add the descriptor
7228 result += (" " + trim(rawUris[innerIdx + 1]));
7229 }
7230
7231 // split the last item into uri and descriptor
7232 var lastTuple = trim(rawUris[i * 2]).split(/\s/);
7233
7234 // sanitize the last uri
7235 result += $$sanitizeUri(trim(lastTuple[0]), true);
7236
7237 // and add the last descriptor if any
7238 if (lastTuple.length === 2) {
7239 result += (" " + trim(lastTuple[1]));
7240 }
7241 this[key] = value = result;
58837242 }
58847243
58857244 if (writeAttr !== false) {
58927251
58937252 // fire observers
58947253 var $$observers = this.$$observers;
5895 $$observers && forEach($$observers[key], function(fn) {
7254 $$observers && forEach($$observers[observer], function(fn) {
58967255 try {
58977256 fn(value);
58987257 } catch (e) {
59177276 * @param {string} key Normalized key. (ie ngAttribute) .
59187277 * @param {function(interpolatedValue)} fn Function that will be called whenever
59197278 the interpolated value of the attribute changes.
5920 * See the {@link guide/directive#Attributes Directives} guide for more info.
5921 * @returns {function()} the `fn` parameter.
7279 * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info.
7280 * @returns {function()} Returns a deregistration function for this observer.
59227281 */
59237282 $observe: function(key, fn) {
59247283 var attrs = this,
5925 $$observers = (attrs.$$observers || (attrs.$$observers = {})),
7284 $$observers = (attrs.$$observers || (attrs.$$observers = createMap())),
59267285 listeners = ($$observers[key] || ($$observers[key] = []));
59277286
59287287 listeners.push(fn);
59297288 $rootScope.$evalAsync(function() {
5930 if (!listeners.$$inter) {
7289 if (!listeners.$$inter && attrs.hasOwnProperty(key)) {
59317290 // no one registered attribute interpolation function, so lets call it manually
59327291 fn(attrs[key]);
59337292 }
59347293 });
5935 return fn;
7294
7295 return function() {
7296 arrayRemove(listeners, fn);
7297 };
59367298 }
59377299 };
7300
7301
7302 function safeAddClass($element, className) {
7303 try {
7304 $element.addClass(className);
7305 } catch (e) {
7306 // ignore, since it means that we are trying to set class on
7307 // SVG element, where class name is read-only.
7308 }
7309 }
7310
59387311
59397312 var startSymbol = $interpolate.startSymbol(),
59407313 endSymbol = $interpolate.endSymbol(),
59457318 },
59467319 NG_ATTR_BINDING = /^ngAttr[A-Z]/;
59477320
7321 compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
7322 var bindings = $element.data('$binding') || [];
7323
7324 if (isArray(binding)) {
7325 bindings = bindings.concat(binding);
7326 } else {
7327 bindings.push(binding);
7328 }
7329
7330 $element.data('$binding', bindings);
7331 } : noop;
7332
7333 compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) {
7334 safeAddClass($element, 'ng-binding');
7335 } : noop;
7336
7337 compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) {
7338 var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
7339 $element.data(dataName, scope);
7340 } : noop;
7341
7342 compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) {
7343 safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope');
7344 } : noop;
59487345
59497346 return compile;
59507347
59597356 }
59607357 // We can not compile top level text elements since text nodes can be merged and we will
59617358 // not be able to attach scope data to them, so we will wrap them in <span>
5962 forEach($compileNodes, function(node, index){
5963 if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
5964 $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];
7359 forEach($compileNodes, function(node, index) {
7360 if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) {
7361 $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
59657362 }
59667363 });
59677364 var compositeLinkFn =
59687365 compileNodes($compileNodes, transcludeFn, $compileNodes,
59697366 maxPriority, ignoreDirective, previousCompileContext);
5970 safeAddClass($compileNodes, 'ng-scope');
5971 return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn){
7367 compile.$$addScopeClass($compileNodes);
7368 var namespace = null;
7369 return function publicLinkFn(scope, cloneConnectFn, options) {
59727370 assertArg(scope, 'scope');
5973 // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
5974 // and sometimes changes the structure of the DOM.
5975 var $linkNode = cloneConnectFn
5976 ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
5977 : $compileNodes;
5978
5979 forEach(transcludeControllers, function(instance, name) {
5980 $linkNode.data('$' + name + 'Controller', instance);
5981 });
5982
5983 // Attach scope only to non-text nodes.
5984 for(var i = 0, ii = $linkNode.length; i<ii; i++) {
5985 var node = $linkNode[i],
5986 nodeType = node.nodeType;
5987 if (nodeType === 1 /* element */ || nodeType === 9 /* document */) {
5988 $linkNode.eq(i).data('$scope', scope);
7371
7372 options = options || {};
7373 var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
7374 transcludeControllers = options.transcludeControllers,
7375 futureParentElement = options.futureParentElement;
7376
7377 // When `parentBoundTranscludeFn` is passed, it is a
7378 // `controllersBoundTransclude` function (it was previously passed
7379 // as `transclude` to directive.link) so we must unwrap it to get
7380 // its `boundTranscludeFn`
7381 if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) {
7382 parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude;
7383 }
7384
7385 if (!namespace) {
7386 namespace = detectNamespaceForChildElements(futureParentElement);
7387 }
7388 var $linkNode;
7389 if (namespace !== 'html') {
7390 // When using a directive with replace:true and templateUrl the $compileNodes
7391 // (or a child element inside of them)
7392 // might change, so we need to recreate the namespace adapted compileNodes
7393 // for call to the link function.
7394 // Note: This will already clone the nodes...
7395 $linkNode = jqLite(
7396 wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html())
7397 );
7398 } else if (cloneConnectFn) {
7399 // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
7400 // and sometimes changes the structure of the DOM.
7401 $linkNode = JQLitePrototype.clone.call($compileNodes);
7402 } else {
7403 $linkNode = $compileNodes;
7404 }
7405
7406 if (transcludeControllers) {
7407 for (var controllerName in transcludeControllers) {
7408 $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance);
59897409 }
59907410 }
7411
7412 compile.$$addScopeInfo($linkNode, scope);
59917413
59927414 if (cloneConnectFn) cloneConnectFn($linkNode, scope);
59937415 if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
59957417 };
59967418 }
59977419
5998 function safeAddClass($element, className) {
5999 try {
6000 $element.addClass(className);
6001 } catch(e) {
6002 // ignore, since it means that we are trying to set class on
6003 // SVG element, where class name is read-only.
7420 function detectNamespaceForChildElements(parentElement) {
7421 // TODO: Make this detect MathML as well...
7422 var node = parentElement && parentElement[0];
7423 if (!node) {
7424 return 'html';
7425 } else {
7426 return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html';
60047427 }
60057428 }
60067429
60227445 function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,
60237446 previousCompileContext) {
60247447 var linkFns = [],
6025 attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound;
7448 attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;
60267449
60277450 for (var i = 0; i < nodeList.length; i++) {
60287451 attrs = new Attributes();
60377460 : null;
60387461
60397462 if (nodeLinkFn && nodeLinkFn.scope) {
6040 safeAddClass(attrs.$$element, 'ng-scope');
7463 compile.$$addScopeClass(attrs.$$element);
60417464 }
60427465
60437466 childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
60497472 (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
60507473 && nodeLinkFn.transclude) : transcludeFn);
60517474
6052 linkFns.push(nodeLinkFn, childLinkFn);
6053 linkFnFound = linkFnFound || nodeLinkFn || childLinkFn;
7475 if (nodeLinkFn || childLinkFn) {
7476 linkFns.push(i, nodeLinkFn, childLinkFn);
7477 linkFnFound = true;
7478 nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn;
7479 }
7480
60547481 //use the previous context only for the first element in the virtual group
60557482 previousCompileContext = null;
60567483 }
60597486 return linkFnFound ? compositeLinkFn : null;
60607487
60617488 function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) {
6062 var nodeLinkFn, childLinkFn, node, childScope, i, ii, n, childBoundTranscludeFn;
6063
6064 // copy nodeList so that linking doesn't break due to live list updates.
6065 var nodeListLength = nodeList.length,
6066 stableNodeList = new Array(nodeListLength);
6067 for (i = 0; i < nodeListLength; i++) {
6068 stableNodeList[i] = nodeList[i];
7489 var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn;
7490 var stableNodeList;
7491
7492
7493 if (nodeLinkFnFound) {
7494 // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our
7495 // offsets don't get screwed up
7496 var nodeListLength = nodeList.length;
7497 stableNodeList = new Array(nodeListLength);
7498
7499 // create a sparse array by only copying the elements which have a linkFn
7500 for (i = 0; i < linkFns.length; i+=3) {
7501 idx = linkFns[i];
7502 stableNodeList[idx] = nodeList[idx];
7503 }
7504 } else {
7505 stableNodeList = nodeList;
60697506 }
60707507
6071 for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
6072 node = stableNodeList[n];
7508 for (i = 0, ii = linkFns.length; i < ii;) {
7509 node = stableNodeList[linkFns[i++]];
60737510 nodeLinkFn = linkFns[i++];
60747511 childLinkFn = linkFns[i++];
60757512
60767513 if (nodeLinkFn) {
60777514 if (nodeLinkFn.scope) {
60787515 childScope = scope.$new();
6079 jqLite.data(node, '$scope', childScope);
7516 compile.$$addScopeInfo(jqLite(node), childScope);
7517 var destroyBindings = nodeLinkFn.$$destroyBindings;
7518 if (destroyBindings) {
7519 nodeLinkFn.$$destroyBindings = null;
7520 childScope.$on('$destroyed', destroyBindings);
7521 }
60807522 } else {
60817523 childScope = scope;
60827524 }
60837525
6084 if ( nodeLinkFn.transcludeOnThisElement ) {
6085 childBoundTranscludeFn = createBoundTranscludeFn(scope, nodeLinkFn.transclude, parentBoundTranscludeFn);
7526 if (nodeLinkFn.transcludeOnThisElement) {
7527 childBoundTranscludeFn = createBoundTranscludeFn(
7528 scope, nodeLinkFn.transclude, parentBoundTranscludeFn);
60867529
60877530 } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
60887531 childBoundTranscludeFn = parentBoundTranscludeFn;
60947537 childBoundTranscludeFn = null;
60957538 }
60967539
6097 nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
7540 nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn,
7541 nodeLinkFn);
60987542
60997543 } else if (childLinkFn) {
61007544 childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
61057549
61067550 function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
61077551
6108 var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) {
6109 var scopeCreated = false;
7552 var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
61107553
61117554 if (!transcludedScope) {
6112 transcludedScope = scope.$new();
7555 transcludedScope = scope.$new(false, containingScope);
61137556 transcludedScope.$$transcluded = true;
6114 scopeCreated = true;
61157557 }
61167558
6117 var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn);
6118 if (scopeCreated) {
6119 clone.on('$destroy', function() { transcludedScope.$destroy(); });
6120 }
6121 return clone;
7559 return transcludeFn(transcludedScope, cloneFn, {
7560 parentBoundTranscludeFn: previousBoundTranscludeFn,
7561 transcludeControllers: controllers,
7562 futureParentElement: futureParentElement
7563 });
61227564 };
61237565
61247566 return boundTranscludeFn;
61407582 match,
61417583 className;
61427584
6143 switch(nodeType) {
6144 case 1: /* Element */
7585 switch (nodeType) {
7586 case NODE_TYPE_ELEMENT: /* Element */
61457587 // use the node name: <directive>
61467588 addDirective(directives,
6147 directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective);
7589 directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective);
61487590
61497591 // iterate over the attributes
61507592 for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
61537595 var attrEndName = false;
61547596
61557597 attr = nAttrs[j];
6156 if (!msie || msie >= 8 || attr.specified) {
6157 name = attr.name;
6158 value = trim(attr.value);
6159
6160 // support ngAttr attribute binding
6161 ngAttrName = directiveNormalize(name);
6162 if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
6163 name = snake_case(ngAttrName.substr(6), '-');
6164 }
6165
6166 var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
7598 name = attr.name;
7599 value = trim(attr.value);
7600
7601 // support ngAttr attribute binding
7602 ngAttrName = directiveNormalize(name);
7603 if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
7604 name = name.replace(PREFIX_REGEXP, '')
7605 .substr(8).replace(/_(.)/g, function(match, letter) {
7606 return letter.toUpperCase();
7607 });
7608 }
7609
7610 var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
7611 if (directiveIsMultiElement(directiveNName)) {
61677612 if (ngAttrName === directiveNName + 'Start') {
61687613 attrStartName = name;
61697614 attrEndName = name.substr(0, name.length - 5) + 'end';
61707615 name = name.substr(0, name.length - 6);
61717616 }
6172
6173 nName = directiveNormalize(name.toLowerCase());
6174 attrsMap[nName] = name;
6175 if (isNgAttr || !attrs.hasOwnProperty(nName)) {
6176 attrs[nName] = value;
6177 if (getBooleanAttrName(node, nName)) {
6178 attrs[nName] = true; // presence means true
6179 }
6180 }
6181 addAttrInterpolateDirective(node, directives, value, nName);
6182 addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
6183 attrEndName);
61847617 }
7618
7619 nName = directiveNormalize(name.toLowerCase());
7620 attrsMap[nName] = name;
7621 if (isNgAttr || !attrs.hasOwnProperty(nName)) {
7622 attrs[nName] = value;
7623 if (getBooleanAttrName(node, nName)) {
7624 attrs[nName] = true; // presence means true
7625 }
7626 }
7627 addAttrInterpolateDirective(node, directives, value, nName, isNgAttr);
7628 addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName,
7629 attrEndName);
61857630 }
61867631
61877632 // use class as directive
61887633 className = node.className;
7634 if (isObject(className)) {
7635 // Maybe SVGAnimatedString
7636 className = className.animVal;
7637 }
61897638 if (isString(className) && className !== '') {
61907639 while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
61917640 nName = directiveNormalize(match[2]);
61967645 }
61977646 }
61987647 break;
6199 case 3: /* Text Node */
7648 case NODE_TYPE_TEXT: /* Text Node */
7649 if (msie === 11) {
7650 // Workaround for #11781
7651 while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) {
7652 node.nodeValue = node.nodeValue + node.nextSibling.nodeValue;
7653 node.parentNode.removeChild(node.nextSibling);
7654 }
7655 }
62007656 addTextInterpolateDirective(directives, node.nodeValue);
62017657 break;
6202 case 8: /* Comment */
7658 case NODE_TYPE_COMMENT: /* Comment */
62037659 try {
62047660 match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
62057661 if (match) {
62327688 var nodes = [];
62337689 var depth = 0;
62347690 if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
6235 var startNode = node;
62367691 do {
62377692 if (!node) {
62387693 throw $compileMinErr('uterdir',
62397694 "Unterminated attribute, found '{0}' but no matching '{1}' found.",
62407695 attrStart, attrEnd);
62417696 }
6242 if (node.nodeType == 1 /** Element **/) {
7697 if (node.nodeType == NODE_TYPE_ELEMENT) {
62437698 if (node.hasAttribute(attrStart)) depth++;
62447699 if (node.hasAttribute(attrEnd)) depth--;
62457700 }
62977752 previousCompileContext = previousCompileContext || {};
62987753
62997754 var terminalPriority = -Number.MAX_VALUE,
6300 newScopeDirective,
7755 newScopeDirective = previousCompileContext.newScopeDirective,
63017756 controllerDirectives = previousCompileContext.controllerDirectives,
63027757 newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
63037758 templateDirective = previousCompileContext.templateDirective,
63157770 directiveValue;
63167771
63177772 // executes all directives on the current element
6318 for(var i = 0, ii = directives.length; i < ii; i++) {
7773 for (var i = 0, ii = directives.length; i < ii; i++) {
63197774 directive = directives[i];
63207775 var attrStart = directive.$$start;
63217776 var attrEnd = directive.$$end;
63317786 }
63327787
63337788 if (directiveValue = directive.scope) {
6334 newScopeDirective = newScopeDirective || directive;
63357789
63367790 // skip the check for directives with async templates, we'll check the derived sync
63377791 // directive when the template arrives
63387792 if (!directive.templateUrl) {
6339 assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive,
6340 $compileNode);
63417793 if (isObject(directiveValue)) {
7794 // This directive is trying to add an isolated scope.
7795 // Check that there is no scope of any kind already
7796 assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective,
7797 directive, $compileNode);
63427798 newIsolateScopeDirective = directive;
7799 } else {
7800 // This directive is trying to add a child scope.
7801 // Check that there is no isolated scope already
7802 assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive,
7803 $compileNode);
63437804 }
63447805 }
7806
7807 newScopeDirective = newScopeDirective || directive;
63457808 }
63467809
63477810 directiveName = directive.name;
63487811
63497812 if (!directive.templateUrl && directive.controller) {
63507813 directiveValue = directive.controller;
6351 controllerDirectives = controllerDirectives || {};
7814 controllerDirectives = controllerDirectives || createMap();
63527815 assertNoDuplicate("'" + directiveName + "' controller",
63537816 controllerDirectives[directiveName], directive, $compileNode);
63547817 controllerDirectives[directiveName] = directive;
64097872 if (jqLiteIsTextNode(directiveValue)) {
64107873 $template = [];
64117874 } else {
6412 $template = jqLite(trim(directiveValue));
7875 $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue)));
64137876 }
64147877 compileNode = $template[0];
64157878
6416 if ($template.length != 1 || compileNode.nodeType !== 1) {
7879 if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
64177880 throw $compileMinErr('tplrt',
64187881 "Template for directive '{0}' must have exactly one root element. {1}",
64197882 directiveName, '');
64557918 nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
64567919 templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
64577920 controllerDirectives: controllerDirectives,
7921 newScopeDirective: (newScopeDirective !== directive) && newScopeDirective,
64587922 newIsolateScopeDirective: newIsolateScopeDirective,
64597923 templateDirective: templateDirective,
64607924 nonTlbTranscludeDirective: nonTlbTranscludeDirective
65157979
65167980
65177981 function getControllers(directiveName, require, $element, elementControllers) {
6518 var value, retrievalMethod = 'data', optional = false;
7982 var value;
7983
65197984 if (isString(require)) {
6520 while((value = require.charAt(0)) == '^' || value == '?') {
6521 require = require.substr(1);
6522 if (value == '^') {
6523 retrievalMethod = 'inheritedData';
6524 }
6525 optional = optional || value == '?';
7985 var match = require.match(REQUIRE_PREFIX_REGEXP);
7986 var name = require.substring(match[0].length);
7987 var inheritType = match[1] || match[3];
7988 var optional = match[2] === '?';
7989
7990 //If only parents then start at the parent element
7991 if (inheritType === '^^') {
7992 $element = $element.parent();
7993 //Otherwise attempt getting the controller from elementControllers in case
7994 //the element is transcluded (and has no data) and to avoid .data if possible
7995 } else {
7996 value = elementControllers && elementControllers[name];
7997 value = value && value.instance;
65267998 }
6527 value = null;
6528
6529 if (elementControllers && retrievalMethod === 'data') {
6530 value = elementControllers[require];
7999
8000 if (!value) {
8001 var dataName = '$' + name + 'Controller';
8002 value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
65318003 }
6532 value = value || $element[retrievalMethod]('$' + require + 'Controller');
65338004
65348005 if (!value && !optional) {
65358006 throw $compileMinErr('ctreq',
65368007 "Controller '{0}', required by directive '{1}', can't be found!",
6537 require, directiveName);
8008 name, directiveName);
65388009 }
6539 return value;
65408010 } else if (isArray(require)) {
65418011 value = [];
6542 forEach(require, function(require) {
6543 value.push(getControllers(directiveName, require, $element, elementControllers));
6544 });
8012 for (var i = 0, ii = require.length; i < ii; i++) {
8013 value[i] = getControllers(directiveName, require[i], $element, elementControllers);
8014 }
65458015 }
6546 return value;
6547 }
6548
6549
6550 function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
6551 var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn;
6552
6553 attrs = (compileNode === linkNode)
6554 ? templateAttrs
6555 : shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr));
6556 $element = attrs.$$element;
8016
8017 return value || null;
8018 }
8019
8020 function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) {
8021 var elementControllers = createMap();
8022 for (var controllerKey in controllerDirectives) {
8023 var directive = controllerDirectives[controllerKey];
8024 var locals = {
8025 $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
8026 $element: $element,
8027 $attrs: attrs,
8028 $transclude: transcludeFn
8029 };
8030
8031 var controller = directive.controller;
8032 if (controller == '@') {
8033 controller = attrs[directive.name];
8034 }
8035
8036 var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
8037
8038 // For directives with element transclusion the element is a comment,
8039 // but jQuery .data doesn't support attaching data to comment nodes as it's hard to
8040 // clean up (http://bugs.jquery.com/ticket/8335).
8041 // Instead, we save the controllers for the element in a local hash and attach to .data
8042 // later, once we have the actual element.
8043 elementControllers[directive.name] = controllerInstance;
8044 if (!hasElementTranscludeDirective) {
8045 $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
8046 }
8047 }
8048 return elementControllers;
8049 }
8050
8051 function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn,
8052 thisLinkFn) {
8053 var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element,
8054 attrs;
8055
8056 if (compileNode === linkNode) {
8057 attrs = templateAttrs;
8058 $element = templateAttrs.$$element;
8059 } else {
8060 $element = jqLite(linkNode);
8061 attrs = new Attributes($element, templateAttrs);
8062 }
65578063
65588064 if (newIsolateScopeDirective) {
6559 var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
6560
65618065 isolateScope = scope.$new(true);
6562
6563 if (templateDirective && (templateDirective === newIsolateScopeDirective ||
6564 templateDirective === newIsolateScopeDirective.$$originalDirective)) {
6565 $element.data('$isolateScope', isolateScope);
6566 } else {
6567 $element.data('$isolateScopeNoTemplate', isolateScope);
8066 }
8067
8068 if (boundTranscludeFn) {
8069 // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn`
8070 // is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
8071 transcludeFn = controllersBoundTransclude;
8072 transcludeFn.$$boundTransclude = boundTranscludeFn;
8073 }
8074
8075 if (controllerDirectives) {
8076 elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope);
8077 }
8078
8079 if (newIsolateScopeDirective) {
8080 // Initialize isolate scope bindings for new isolate scope directive.
8081 compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective ||
8082 templateDirective === newIsolateScopeDirective.$$originalDirective)));
8083 compile.$$addScopeClass($element, true);
8084 isolateScope.$$isolateBindings =
8085 newIsolateScopeDirective.$$isolateBindings;
8086 initializeDirectiveBindings(scope, attrs, isolateScope,
8087 isolateScope.$$isolateBindings,
8088 newIsolateScopeDirective, isolateScope);
8089 }
8090 if (elementControllers) {
8091 // Initialize bindToController bindings for new/isolate scopes
8092 var scopeDirective = newIsolateScopeDirective || newScopeDirective;
8093 var bindings;
8094 var controllerForBindings;
8095 if (scopeDirective && elementControllers[scopeDirective.name]) {
8096 bindings = scopeDirective.$$bindings.bindToController;
8097 controller = elementControllers[scopeDirective.name];
8098
8099 if (controller && controller.identifier && bindings) {
8100 controllerForBindings = controller;
8101 thisLinkFn.$$destroyBindings =
8102 initializeDirectiveBindings(scope, attrs, controller.instance,
8103 bindings, scopeDirective);
8104 }
65688105 }
6569
6570
6571
6572 safeAddClass($element, 'ng-isolate-scope');
6573
6574 forEach(newIsolateScopeDirective.scope, function(definition, scopeName) {
6575 var match = definition.match(LOCAL_REGEXP) || [],
6576 attrName = match[3] || scopeName,
6577 optional = (match[2] == '?'),
6578 mode = match[1], // @, =, or &
6579 lastValue,
6580 parentGet, parentSet, compare;
6581
6582 isolateScope.$$isolateBindings[scopeName] = mode + attrName;
6583
6584 switch (mode) {
6585
6586 case '@':
6587 attrs.$observe(attrName, function(value) {
6588 isolateScope[scopeName] = value;
6589 });
6590 attrs.$$observers[attrName].$$scope = scope;
6591 if( attrs[attrName] ) {
6592 // If the attribute has been provided then we trigger an interpolation to ensure
6593 // the value is there for use in the link fn
6594 isolateScope[scopeName] = $interpolate(attrs[attrName])(scope);
6595 }
6596 break;
6597
6598 case '=':
6599 if (optional && !attrs[attrName]) {
6600 return;
6601 }
6602 parentGet = $parse(attrs[attrName]);
6603 if (parentGet.literal) {
6604 compare = equals;
6605 } else {
6606 compare = function(a,b) { return a === b || (a !== a && b !== b); };
6607 }
6608 parentSet = parentGet.assign || function() {
6609 // reset the change, or we will throw this exception on every $digest
6610 lastValue = isolateScope[scopeName] = parentGet(scope);
6611 throw $compileMinErr('nonassign',
6612 "Expression '{0}' used with directive '{1}' is non-assignable!",
6613 attrs[attrName], newIsolateScopeDirective.name);
6614 };
6615 lastValue = isolateScope[scopeName] = parentGet(scope);
6616 isolateScope.$watch(function parentValueWatch() {
6617 var parentValue = parentGet(scope);
6618 if (!compare(parentValue, isolateScope[scopeName])) {
6619 // we are out of sync and need to copy
6620 if (!compare(parentValue, lastValue)) {
6621 // parent changed and it has precedence
6622 isolateScope[scopeName] = parentValue;
6623 } else {
6624 // if the parent can be assigned then do so
6625 parentSet(scope, parentValue = isolateScope[scopeName]);
6626 }
6627 }
6628 return lastValue = parentValue;
6629 }, null, parentGet.literal);
6630 break;
6631
6632 case '&':
6633 parentGet = $parse(attrs[attrName]);
6634 isolateScope[scopeName] = function(locals) {
6635 return parentGet(scope, locals);
6636 };
6637 break;
6638
6639 default:
6640 throw $compileMinErr('iscp',
6641 "Invalid isolate scope definition for directive '{0}'." +
6642 " Definition: {... {1}: '{2}' ...}",
6643 newIsolateScopeDirective.name, scopeName, definition);
8106 for (i in elementControllers) {
8107 controller = elementControllers[i];
8108 var controllerResult = controller();
8109
8110 if (controllerResult !== controller.instance) {
8111 // If the controller constructor has a return value, overwrite the instance
8112 // from setupControllers and update the element data
8113 controller.instance = controllerResult;
8114 $element.data('$' + i + 'Controller', controllerResult);
8115 if (controller === controllerForBindings) {
8116 // Remove and re-install bindToController bindings
8117 thisLinkFn.$$destroyBindings();
8118 thisLinkFn.$$destroyBindings =
8119 initializeDirectiveBindings(scope, attrs, controllerResult, bindings, scopeDirective);
8120 }
66448121 }
6645 });
8122 }
66468123 }
6647 transcludeFn = boundTranscludeFn && controllersBoundTransclude;
6648 if (controllerDirectives) {
6649 forEach(controllerDirectives, function(directive) {
6650 var locals = {
6651 $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
6652 $element: $element,
6653 $attrs: attrs,
6654 $transclude: transcludeFn
6655 }, controllerInstance;
6656
6657 controller = directive.controller;
6658 if (controller == '@') {
6659 controller = attrs[directive.name];
6660 }
6661
6662 controllerInstance = $controller(controller, locals);
6663 // For directives with element transclusion the element is a comment,
6664 // but jQuery .data doesn't support attaching data to comment nodes as it's hard to
6665 // clean up (http://bugs.jquery.com/ticket/8335).
6666 // Instead, we save the controllers for the element in a local hash and attach to .data
6667 // later, once we have the actual element.
6668 elementControllers[directive.name] = controllerInstance;
6669 if (!hasElementTranscludeDirective) {
6670 $element.data('$' + directive.name + 'Controller', controllerInstance);
6671 }
6672
6673 if (directive.controllerAs) {
6674 locals.$scope[directive.controllerAs] = controllerInstance;
6675 }
6676 });
6677 }
66788124
66798125 // PRELINKING
6680 for(i = 0, ii = preLinkFns.length; i < ii; i++) {
6681 try {
6682 linkFn = preLinkFns[i];
6683 linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
6684 linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn);
6685 } catch (e) {
6686 $exceptionHandler(e, startingTag($element));
6687 }
8126 for (i = 0, ii = preLinkFns.length; i < ii; i++) {
8127 linkFn = preLinkFns[i];
8128 invokeLinkFn(linkFn,
8129 linkFn.isolateScope ? isolateScope : scope,
8130 $element,
8131 attrs,
8132 linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
8133 transcludeFn
8134 );
66888135 }
66898136
66908137 // RECURSION
66978144 childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
66988145
66998146 // POSTLINKING
6700 for(i = postLinkFns.length - 1; i >= 0; i--) {
6701 try {
6702 linkFn = postLinkFns[i];
6703 linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
6704 linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn);
6705 } catch (e) {
6706 $exceptionHandler(e, startingTag($element));
6707 }
8147 for (i = postLinkFns.length - 1; i >= 0; i--) {
8148 linkFn = postLinkFns[i];
8149 invokeLinkFn(linkFn,
8150 linkFn.isolateScope ? isolateScope : scope,
8151 $element,
8152 attrs,
8153 linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers),
8154 transcludeFn
8155 );
67088156 }
67098157
67108158 // This is the function that is injected as `$transclude`.
6711 function controllersBoundTransclude(scope, cloneAttachFn) {
8159 // Note: all arguments are optional!
8160 function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) {
67128161 var transcludeControllers;
67138162
6714 // no scope passed
6715 if (arguments.length < 2) {
8163 // No scope passed in:
8164 if (!isScope(scope)) {
8165 futureParentElement = cloneAttachFn;
67168166 cloneAttachFn = scope;
67178167 scope = undefined;
67188168 }
67208170 if (hasElementTranscludeDirective) {
67218171 transcludeControllers = elementControllers;
67228172 }
6723
6724 return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers);
8173 if (!futureParentElement) {
8174 futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
8175 }
8176 return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
67258177 }
67268178 }
67278179 }
67528204 if (name === ignoreDirective) return null;
67538205 var match = null;
67548206 if (hasDirectives.hasOwnProperty(name)) {
6755 for(var directive, directives = $injector.get(name + Suffix),
6756 i = 0, ii = directives.length; i<ii; i++) {
8207 for (var directive, directives = $injector.get(name + Suffix),
8208 i = 0, ii = directives.length; i < ii; i++) {
67578209 try {
67588210 directive = directives[i];
6759 if ( (maxPriority === undefined || maxPriority > directive.priority) &&
8211 if ((maxPriority === undefined || maxPriority > directive.priority) &&
67608212 directive.restrict.indexOf(location) != -1) {
67618213 if (startAttrName) {
67628214 directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
67648216 tDirectives.push(directive);
67658217 match = directive;
67668218 }
6767 } catch(e) { $exceptionHandler(e); }
8219 } catch (e) { $exceptionHandler(e); }
67688220 }
67698221 }
67708222 return match;
67718223 }
67728224
8225
8226 /**
8227 * looks up the directive and returns true if it is a multi-element directive,
8228 * and therefore requires DOM nodes between -start and -end markers to be grouped
8229 * together.
8230 *
8231 * @param {string} name name of the directive to look up.
8232 * @returns true if directive was registered as multi-element.
8233 */
8234 function directiveIsMultiElement(name) {
8235 if (hasDirectives.hasOwnProperty(name)) {
8236 for (var directive, directives = $injector.get(name + Suffix),
8237 i = 0, ii = directives.length; i < ii; i++) {
8238 directive = directives[i];
8239 if (directive.multiElement) {
8240 return true;
8241 }
8242 }
8243 }
8244 return false;
8245 }
67738246
67748247 /**
67758248 * When the element is replaced with HTML template then the new attributes
68208293 afterTemplateChildLinkFn,
68218294 beforeTemplateCompileNode = $compileNode[0],
68228295 origAsyncDirective = directives.shift(),
6823 // The fact that we have to copy and patch the directive seems wrong!
6824 derivedSyncDirective = extend({}, origAsyncDirective, {
8296 derivedSyncDirective = inherit(origAsyncDirective, {
68258297 templateUrl: null, transclude: null, replace: null, $$originalDirective: origAsyncDirective
68268298 }),
68278299 templateUrl = (isFunction(origAsyncDirective.templateUrl))
68288300 ? origAsyncDirective.templateUrl($compileNode, tAttrs)
6829 : origAsyncDirective.templateUrl;
8301 : origAsyncDirective.templateUrl,
8302 templateNamespace = origAsyncDirective.templateNamespace;
68308303
68318304 $compileNode.empty();
68328305
6833 $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}).
6834 success(function(content) {
8306 $templateRequest(templateUrl)
8307 .then(function(content) {
68358308 var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn;
68368309
68378310 content = denormalizeTemplate(content);
68408313 if (jqLiteIsTextNode(content)) {
68418314 $template = [];
68428315 } else {
6843 $template = jqLite(trim(content));
8316 $template = removeComments(wrapTemplate(templateNamespace, trim(content)));
68448317 }
68458318 compileNode = $template[0];
68468319
6847 if ($template.length != 1 || compileNode.nodeType !== 1) {
8320 if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
68488321 throw $compileMinErr('tplrt',
68498322 "Template for directive '{0}' must have exactly one root element. {1}",
68508323 origAsyncDirective.name, templateUrl);
68768349 });
68778350 afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
68788351
6879 while(linkQueue.length) {
8352 while (linkQueue.length) {
68808353 var scope = linkQueue.shift(),
68818354 beforeTemplateLinkNode = linkQueue.shift(),
68828355 linkRootElement = linkQueue.shift(),
68838356 boundTranscludeFn = linkQueue.shift(),
68848357 linkNode = $compileNode[0];
68858358
8359 if (scope.$$destroyed) continue;
8360
68868361 if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
68878362 var oldClasses = beforeTemplateLinkNode.className;
68888363
68918366 // it was cloned therefore we have to clone as well.
68928367 linkNode = jqLiteClone(compileNode);
68938368 }
6894
68958369 replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
68968370
68978371 // Copy in CSS classes from original node
69038377 childBoundTranscludeFn = boundTranscludeFn;
69048378 }
69058379 afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement,
6906 childBoundTranscludeFn);
8380 childBoundTranscludeFn, afterTemplateNodeLinkFn);
69078381 }
69088382 linkQueue = null;
6909 }).
6910 error(function(response, code, headers, config) {
6911 throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url);
69128383 });
69138384
69148385 return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
69158386 var childBoundTranscludeFn = boundTranscludeFn;
8387 if (scope.$$destroyed) return;
69168388 if (linkQueue) {
6917 linkQueue.push(scope);
6918 linkQueue.push(node);
6919 linkQueue.push(rootElement);
6920 linkQueue.push(childBoundTranscludeFn);
8389 linkQueue.push(scope,
8390 node,
8391 rootElement,
8392 childBoundTranscludeFn);
69218393 } else {
69228394 if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
69238395 childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
69248396 }
6925 afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
8397 afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn,
8398 afterTemplateNodeLinkFn);
69268399 }
69278400 };
69288401 }
69388411 return a.index - b.index;
69398412 }
69408413
6941
69428414 function assertNoDuplicate(what, previousDirective, directive, element) {
8415
8416 function wrapModuleNameIfDefined(moduleName) {
8417 return moduleName ?
8418 (' (module: ' + moduleName + ')') :
8419 '';
8420 }
8421
69438422 if (previousDirective) {
6944 throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}',
6945 previousDirective.name, directive.name, what, startingTag(element));
6946 }
6947 }
6948
6949
6950 function addTextInterpolateDirective(directives, text) {
6951 var interpolateFn = $interpolate(text, true);
6952 if (interpolateFn) {
6953 directives.push({
6954 priority: 0,
6955 compile: function textInterpolateCompileFn(templateNode) {
6956 // when transcluding a template that has bindings in the root
6957 // then we don't have a parent and should do this in the linkFn
6958 var parent = templateNode.parent(), hasCompileParent = parent.length;
6959 if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding');
6960
6961 return function textInterpolateLinkFn(scope, node) {
6962 var parent = node.parent(),
6963 bindings = parent.data('$binding') || [];
6964 bindings.push(interpolateFn);
6965 parent.data('$binding', bindings);
6966 if (!hasCompileParent) safeAddClass(parent, 'ng-binding');
6967 scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
6968 node[0].nodeValue = value;
6969 });
6970 };
6971 }
6972 });
6973 }
6974 }
8423 throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}',
8424 previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName),
8425 directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element));
8426 }
8427 }
8428
8429
8430 function addTextInterpolateDirective(directives, text) {
8431 var interpolateFn = $interpolate(text, true);
8432 if (interpolateFn) {
8433 directives.push({
8434 priority: 0,
8435 compile: function textInterpolateCompileFn(templateNode) {
8436 var templateNodeParent = templateNode.parent(),
8437 hasCompileParent = !!templateNodeParent.length;
8438
8439 // When transcluding a template that has bindings in the root
8440 // we don't have a parent and thus need to add the class during linking fn.
8441 if (hasCompileParent) compile.$$addBindingClass(templateNodeParent);
8442
8443 return function textInterpolateLinkFn(scope, node) {
8444 var parent = node.parent();
8445 if (!hasCompileParent) compile.$$addBindingClass(parent);
8446 compile.$$addBindingInfo(parent, interpolateFn.expressions);
8447 scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
8448 node[0].nodeValue = value;
8449 });
8450 };
8451 }
8452 });
8453 }
8454 }
8455
8456
8457 function wrapTemplate(type, template) {
8458 type = lowercase(type || 'html');
8459 switch (type) {
8460 case 'svg':
8461 case 'math':
8462 var wrapper = document.createElement('div');
8463 wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>';
8464 return wrapper.childNodes[0].childNodes;
8465 default:
8466 return template;
8467 }
8468 }
69758469
69768470
69778471 function getTrustedContext(node, attrNormalizedName) {
69818475 var tag = nodeName_(node);
69828476 // maction[xlink:href] can source SVG. It's not limited to <maction>.
69838477 if (attrNormalizedName == "xlinkHref" ||
6984 (tag == "FORM" && attrNormalizedName == "action") ||
6985 (tag != "IMG" && (attrNormalizedName == "src" ||
8478 (tag == "form" && attrNormalizedName == "action") ||
8479 (tag != "img" && (attrNormalizedName == "src" ||
69868480 attrNormalizedName == "ngSrc"))) {
69878481 return $sce.RESOURCE_URL;
69888482 }
69898483 }
69908484
69918485
6992 function addAttrInterpolateDirective(node, directives, value, name) {
6993 var interpolateFn = $interpolate(value, true);
8486 function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) {
8487 var trustedContext = getTrustedContext(node, name);
8488 allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing;
8489
8490 var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing);
69948491
69958492 // no interpolation found -> ignore
69968493 if (!interpolateFn) return;
69978494
69988495
6999 if (name === "multiple" && nodeName_(node) === "SELECT") {
8496 if (name === "multiple" && nodeName_(node) === "select") {
70008497 throw $compileMinErr("selmulti",
70018498 "Binding to the 'multiple' attribute is not supported. Element: {0}",
70028499 startingTag(node));
70158512 "ng- versions (such as ng-click instead of onclick) instead.");
70168513 }
70178514
7018 // we need to interpolate again, in case the attribute value has been updated
7019 // (e.g. by another directive's compile function)
7020 interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name));
8515 // If the attribute has changed since last $interpolate()ed
8516 var newValue = attr[name];
8517 if (newValue !== value) {
8518 // we need to interpolate again since the attribute value has been updated
8519 // (e.g. by another directive's compile function)
8520 // ensure unset/empty values make interpolateFn falsy
8521 interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing);
8522 value = newValue;
8523 }
70218524
70228525 // if attribute was updated so that there is no interpolation going on we don't want to
70238526 // register any observers
70248527 if (!interpolateFn) return;
70258528
7026 // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the
7027 // actual attr value
8529 // initialize attr object so that it's ready in case we need the value for isolate
8530 // scope initialization, otherwise the value would not be available from isolate
8531 // directive's linking fn during linking phase
70288532 attr[name] = interpolateFn(scope);
8533
70298534 ($$observers[name] || ($$observers[name] = [])).$$inter = true;
70308535 (attr.$$observers && attr.$$observers[name].$$scope || scope).
70318536 $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
70358540 //skip animations when the first digest occurs (when
70368541 //both the new and the old values are the same) since
70378542 //the CSS classes are the non-interpolated values
7038 if(name === 'class' && newValue != oldValue) {
8543 if (name === 'class' && newValue != oldValue) {
70398544 attr.$updateClass(newValue, oldValue);
70408545 } else {
70418546 attr.$set(name, newValue);
70658570 i, ii;
70668571
70678572 if ($rootElement) {
7068 for(i = 0, ii = $rootElement.length; i < ii; i++) {
8573 for (i = 0, ii = $rootElement.length; i < ii; i++) {
70698574 if ($rootElement[i] == firstElementToRemove) {
70708575 $rootElement[i++] = newNode;
70718576 for (var j = i, j2 = j + removeCount - 1,
70788583 }
70798584 }
70808585 $rootElement.length -= removeCount - 1;
8586
8587 // If the replaced element is also the jQuery .context then replace it
8588 // .context is a deprecated jQuery api, so we should set it only when jQuery set it
8589 // http://api.jquery.com/context/
8590 if ($rootElement.context === firstElementToRemove) {
8591 $rootElement.context = newNode;
8592 }
70818593 break;
70828594 }
70838595 }
70868598 if (parent) {
70878599 parent.replaceChild(newNode, firstElementToRemove);
70888600 }
8601
8602 // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it?
70898603 var fragment = document.createDocumentFragment();
70908604 fragment.appendChild(firstElementToRemove);
7091 newNode[jqLite.expando] = firstElementToRemove[jqLite.expando];
8605
8606 if (jqLite.hasData(firstElementToRemove)) {
8607 // Copy over user data (that includes Angular's $scope etc.). Don't copy private
8608 // data here because there's no public interface in jQuery to do that and copying over
8609 // event listeners (which is the main use of private data) wouldn't work anyway.
8610 jqLite(newNode).data(jqLite(firstElementToRemove).data());
8611
8612 // Remove data of the replaced element. We cannot just call .remove()
8613 // on the element it since that would deallocate scope that is needed
8614 // for the new node. Instead, remove the data "manually".
8615 if (!jQuery) {
8616 delete jqLite.cache[firstElementToRemove[jqLite.expando]];
8617 } else {
8618 // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after
8619 // the replaced element. The cleanData version monkey-patched by Angular would cause
8620 // the scope to be trashed and we do need the very same scope to work with the new
8621 // element. However, we cannot just cache the non-patched version and use it here as
8622 // that would break if another library patches the method after Angular does (one
8623 // example is jQuery UI). Instead, set a flag indicating scope destroying should be
8624 // skipped this one time.
8625 skipDestroyOnNextJQueryCleanData = true;
8626 jQuery.cleanData([firstElementToRemove]);
8627 }
8628 }
8629
70928630 for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
70938631 var element = elementsToRemove[k];
70948632 jqLite(element).remove(); // must do this way to clean up expando
71048642 function cloneAndAnnotateFn(fn, annotation) {
71058643 return extend(function() { return fn.apply(null, arguments); }, fn, annotation);
71068644 }
8645
8646
8647 function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) {
8648 try {
8649 linkFn(scope, $element, attrs, controllers, transcludeFn);
8650 } catch (e) {
8651 $exceptionHandler(e, startingTag($element));
8652 }
8653 }
8654
8655
8656 // Set up $watches for isolate scope and controller bindings. This process
8657 // only occurs for isolate scopes and new scopes with controllerAs.
8658 function initializeDirectiveBindings(scope, attrs, destination, bindings,
8659 directive, newScope) {
8660 var onNewScopeDestroyed;
8661 forEach(bindings, function(definition, scopeName) {
8662 var attrName = definition.attrName,
8663 optional = definition.optional,
8664 mode = definition.mode, // @, =, or &
8665 lastValue,
8666 parentGet, parentSet, compare;
8667
8668 if (!hasOwnProperty.call(attrs, attrName)) {
8669 // In the case of user defined a binding with the same name as a method in Object.prototype but didn't set
8670 // the corresponding attribute. We need to make sure subsequent code won't access to the prototype function
8671 attrs[attrName] = undefined;
8672 }
8673
8674 switch (mode) {
8675
8676 case '@':
8677 if (!attrs[attrName] && !optional) {
8678 destination[scopeName] = undefined;
8679 }
8680
8681 attrs.$observe(attrName, function(value) {
8682 destination[scopeName] = value;
8683 });
8684 attrs.$$observers[attrName].$$scope = scope;
8685 if (attrs[attrName]) {
8686 // If the attribute has been provided then we trigger an interpolation to ensure
8687 // the value is there for use in the link fn
8688 destination[scopeName] = $interpolate(attrs[attrName])(scope);
8689 }
8690 break;
8691
8692 case '=':
8693 if (optional && !attrs[attrName]) {
8694 return;
8695 }
8696 parentGet = $parse(attrs[attrName]);
8697
8698 if (parentGet.literal) {
8699 compare = equals;
8700 } else {
8701 compare = function(a, b) { return a === b || (a !== a && b !== b); };
8702 }
8703 parentSet = parentGet.assign || function() {
8704 // reset the change, or we will throw this exception on every $digest
8705 lastValue = destination[scopeName] = parentGet(scope);
8706 throw $compileMinErr('nonassign',
8707 "Expression '{0}' used with directive '{1}' is non-assignable!",
8708 attrs[attrName], directive.name);
8709 };
8710 lastValue = destination[scopeName] = parentGet(scope);
8711 var parentValueWatch = function parentValueWatch(parentValue) {
8712 if (!compare(parentValue, destination[scopeName])) {
8713 // we are out of sync and need to copy
8714 if (!compare(parentValue, lastValue)) {
8715 // parent changed and it has precedence
8716 destination[scopeName] = parentValue;
8717 } else {
8718 // if the parent can be assigned then do so
8719 parentSet(scope, parentValue = destination[scopeName]);
8720 }
8721 }
8722 return lastValue = parentValue;
8723 };
8724 parentValueWatch.$stateful = true;
8725 var unwatch;
8726 if (definition.collection) {
8727 unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
8728 } else {
8729 unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
8730 }
8731 onNewScopeDestroyed = (onNewScopeDestroyed || []);
8732 onNewScopeDestroyed.push(unwatch);
8733 break;
8734
8735 case '&':
8736 parentGet = $parse(attrs[attrName]);
8737
8738 // Don't assign noop to destination if expression is not valid
8739 if (parentGet === noop && optional) break;
8740
8741 destination[scopeName] = function(locals) {
8742 return parentGet(scope, locals);
8743 };
8744 break;
8745 }
8746 });
8747 var destroyBindings = onNewScopeDestroyed ? function destroyBindings() {
8748 for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) {
8749 onNewScopeDestroyed[i]();
8750 }
8751 } : noop;
8752 if (newScope && destroyBindings !== noop) {
8753 newScope.$on('$destroy', destroyBindings);
8754 return noop;
8755 }
8756 return destroyBindings;
8757 }
71078758 }];
71088759 }
71098760
7110 var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i;
8761 var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
71118762 /**
71128763 * Converts all accepted directives format into proper directive name.
7113 * All of these will become 'myDirective':
7114 * my:Directive
7115 * my-directive
7116 * x-my-directive
7117 * data-my:directive
7118 *
7119 * Also there is special case for Moz prefix starting with upper case letter.
71208764 * @param name Name to normalize
71218765 */
71228766 function directiveNormalize(name) {
71738817 /* NodeList */ nodeList,
71748818 /* Element */ rootElement,
71758819 /* function(Function) */ boundTranscludeFn
7176 ){}
8820 ) {}
71778821
71788822 function directiveLinkingFn(
71798823 /* nodesetLinkingFn */ nodesetLinkingFn,
71818825 /* Node */ node,
71828826 /* Element */ rootElement,
71838827 /* function(Function) */ boundTranscludeFn
7184 ){}
8828 ) {}
71858829
71868830 function tokenDifference(str1, str2) {
71878831 var values = '',
71898833 tokens2 = str2.split(/\s+/);
71908834
71918835 outer:
7192 for(var i = 0; i < tokens1.length; i++) {
8836 for (var i = 0; i < tokens1.length; i++) {
71938837 var token = tokens1[i];
7194 for(var j = 0; j < tokens2.length; j++) {
7195 if(token == tokens2[j]) continue outer;
8838 for (var j = 0; j < tokens2.length; j++) {
8839 if (token == tokens2[j]) continue outer;
71968840 }
71978841 values += (values.length > 0 ? ' ' : '') + token;
71988842 }
71998843 return values;
72008844 }
8845
8846 function removeComments(jqNodes) {
8847 jqNodes = jqLite(jqNodes);
8848 var i = jqNodes.length;
8849
8850 if (i <= 1) {
8851 return jqNodes;
8852 }
8853
8854 while (i--) {
8855 var node = jqNodes[i];
8856 if (node.nodeType === NODE_TYPE_COMMENT) {
8857 splice.call(jqNodes, i, 1);
8858 }
8859 }
8860 return jqNodes;
8861 }
8862
8863 var $controllerMinErr = minErr('$controller');
8864
8865
8866 var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/;
8867 function identifierForController(controller, ident) {
8868 if (ident && isString(ident)) return ident;
8869 if (isString(controller)) {
8870 var match = CNTRL_REG.exec(controller);
8871 if (match) return match[3];
8872 }
8873 }
8874
72018875
72028876 /**
72038877 * @ngdoc provider
72118885 */
72128886 function $ControllerProvider() {
72138887 var controllers = {},
7214 CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/;
7215
8888 globals = false;
72168889
72178890 /**
72188891 * @ngdoc method
72318904 }
72328905 };
72338906
8907 /**
8908 * @ngdoc method
8909 * @name $controllerProvider#allowGlobals
8910 * @description If called, allows `$controller` to find controller constructors on `window`
8911 */
8912 this.allowGlobals = function() {
8913 globals = true;
8914 };
8915
72348916
72358917 this.$get = ['$injector', '$window', function($injector, $window) {
72368918
72458927 *
72468928 * * check if a controller with given name is registered via `$controllerProvider`
72478929 * * check if evaluating the string on the current scope returns a constructor
7248 * * check `window[constructor]` on the global `window` object
8930 * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
8931 * `window` object (not recommended)
8932 *
8933 * The string can use the `controller as property` syntax, where the controller instance is published
8934 * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
8935 * to work correctly.
72498936 *
72508937 * @param {Object} locals Injection locals for Controller.
72518938 * @return {Object} Instance of given controller.
72568943 * It's just a simple call to {@link auto.$injector $injector}, but extracted into
72578944 * a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
72588945 */
7259 return function(expression, locals) {
8946 return function(expression, locals, later, ident) {
8947 // PRIVATE API:
8948 // param `later` --- indicates that the controller's constructor is invoked at a later time.
8949 // If true, $controller will allocate the object with the correct
8950 // prototype chain, but will not invoke the controller until a returned
8951 // callback is invoked.
8952 // param `ident` --- An optional label which overrides the label parsed from the controller
8953 // expression, if any.
72608954 var instance, match, constructor, identifier;
7261
7262 if(isString(expression)) {
7263 match = expression.match(CNTRL_REG),
8955 later = later === true;
8956 if (ident && isString(ident)) {
8957 identifier = ident;
8958 }
8959
8960 if (isString(expression)) {
8961 match = expression.match(CNTRL_REG);
8962 if (!match) {
8963 throw $controllerMinErr('ctrlfmt',
8964 "Badly formed controller string '{0}'. " +
8965 "Must match `__name__ as __id__` or `__name__`.", expression);
8966 }
72648967 constructor = match[1],
7265 identifier = match[3];
8968 identifier = identifier || match[3];
72668969 expression = controllers.hasOwnProperty(constructor)
72678970 ? controllers[constructor]
7268 : getter(locals.$scope, constructor, true) || getter($window, constructor, true);
8971 : getter(locals.$scope, constructor, true) ||
8972 (globals ? getter($window, constructor, true) : undefined);
72698973
72708974 assertArgFn(expression, constructor, true);
72718975 }
72728976
7273 instance = $injector.instantiate(expression, locals);
8977 if (later) {
8978 // Instantiate controller later:
8979 // This machinery is used to create an instance of the object before calling the
8980 // controller's constructor itself.
8981 //
8982 // This allows properties to be added to the controller before the constructor is
8983 // invoked. Primarily, this is used for isolate scope bindings in $compile.
8984 //
8985 // This feature is not intended for use by applications, and is thus not documented
8986 // publicly.
8987 // Object creation: http://jsperf.com/create-constructor/2
8988 var controllerPrototype = (isArray(expression) ?
8989 expression[expression.length - 1] : expression).prototype;
8990 instance = Object.create(controllerPrototype || null);
8991
8992 if (identifier) {
8993 addIdentifier(locals, identifier, instance, constructor || expression.name);
8994 }
8995
8996 var instantiate;
8997 return instantiate = extend(function() {
8998 var result = $injector.invoke(expression, instance, locals, constructor);
8999 if (result !== instance && (isObject(result) || isFunction(result))) {
9000 instance = result;
9001 if (identifier) {
9002 // If result changed, re-assign controllerAs value to scope.
9003 addIdentifier(locals, identifier, instance, constructor || expression.name);
9004 }
9005 }
9006 return instance;
9007 }, {
9008 instance: instance,
9009 identifier: identifier
9010 });
9011 }
9012
9013 instance = $injector.instantiate(expression, locals, constructor);
72749014
72759015 if (identifier) {
7276 if (!(locals && typeof locals.$scope === 'object')) {
7277 throw minErr('$controller')('noscp',
7278 "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
7279 constructor || expression.name, identifier);
7280 }
7281
7282 locals.$scope[identifier] = instance;
9016 addIdentifier(locals, identifier, instance, constructor || expression.name);
72839017 }
72849018
72859019 return instance;
72869020 };
9021
9022 function addIdentifier(locals, identifier, instance, name) {
9023 if (!(locals && isObject(locals.$scope))) {
9024 throw minErr('$controller')('noscp',
9025 "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
9026 name, identifier);
9027 }
9028
9029 locals.$scope[identifier] = instance;
9030 }
72879031 }];
72889032 }
72899033
73129056 </file>
73139057 </example>
73149058 */
7315 function $DocumentProvider(){
7316 this.$get = ['$window', function(window){
9059 function $DocumentProvider() {
9060 this.$get = ['$window', function(window) {
73179061 return jqLite(window.document);
73189062 }];
73199063 }
73349078 * ## Example:
73359079 *
73369080 * ```js
7337 * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () {
7338 * return function (exception, cause) {
9081 * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
9082 * return function(exception, cause) {
73399083 * exception.message += ' (caused by "' + cause + '")';
73409084 * throw exception;
73419085 * };
73449088 *
73459089 * This example will override the normal action of `$exceptionHandler`, to make angular
73469090 * exceptions fail hard when they happen, instead of just logging to the console.
9091 *
9092 * <hr />
9093 * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind`
9094 * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler}
9095 * (unless executed during a digest).
9096 *
9097 * If you wish, you can manually delegate exceptions, e.g.
9098 * `try { ... } catch(e) { $exceptionHandler(e); }`
73479099 *
73489100 * @param {Error} exception Exception associated with the error.
73499101 * @param {string=} cause optional information about the context in which
73589110 }];
73599111 }
73609112
9113 var APPLICATION_JSON = 'application/json';
9114 var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'};
9115 var JSON_START = /^\[|^\{(?!\{)/;
9116 var JSON_ENDS = {
9117 '[': /]$/,
9118 '{': /}$/
9119 };
9120 var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
9121
9122 function serializeValue(v) {
9123 if (isObject(v)) {
9124 return isDate(v) ? v.toISOString() : toJson(v);
9125 }
9126 return v;
9127 }
9128
9129
9130 function $HttpParamSerializerProvider() {
9131 /**
9132 * @ngdoc service
9133 * @name $httpParamSerializer
9134 * @description
9135 *
9136 * Default {@link $http `$http`} params serializer that converts objects to strings
9137 * according to the following rules:
9138 *
9139 * * `{'foo': 'bar'}` results in `foo=bar`
9140 * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object)
9141 * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element)
9142 * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object)
9143 *
9144 * Note that serializer will sort the request parameters alphabetically.
9145 * */
9146
9147 this.$get = function() {
9148 return function ngParamSerializer(params) {
9149 if (!params) return '';
9150 var parts = [];
9151 forEachSorted(params, function(value, key) {
9152 if (value === null || isUndefined(value)) return;
9153 if (isArray(value)) {
9154 forEach(value, function(v, k) {
9155 parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
9156 });
9157 } else {
9158 parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value)));
9159 }
9160 });
9161
9162 return parts.join('&');
9163 };
9164 };
9165 }
9166
9167 function $HttpParamSerializerJQLikeProvider() {
9168 /**
9169 * @ngdoc service
9170 * @name $httpParamSerializerJQLike
9171 * @description
9172 *
9173 * Alternative {@link $http `$http`} params serializer that follows
9174 * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic.
9175 * The serializer will also sort the params alphabetically.
9176 *
9177 * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property:
9178 *
9179 * ```js
9180 * $http({
9181 * url: myUrl,
9182 * method: 'GET',
9183 * params: myParams,
9184 * paramSerializer: '$httpParamSerializerJQLike'
9185 * });
9186 * ```
9187 *
9188 * It is also possible to set it as the default `paramSerializer` in the
9189 * {@link $httpProvider#defaults `$httpProvider`}.
9190 *
9191 * Additionally, you can inject the serializer and use it explicitly, for example to serialize
9192 * form data for submission:
9193 *
9194 * ```js
9195 * .controller(function($http, $httpParamSerializerJQLike) {
9196 * //...
9197 *
9198 * $http({
9199 * url: myUrl,
9200 * method: 'POST',
9201 * data: $httpParamSerializerJQLike(myData),
9202 * headers: {
9203 * 'Content-Type': 'application/x-www-form-urlencoded'
9204 * }
9205 * });
9206 *
9207 * });
9208 * ```
9209 *
9210 * */
9211 this.$get = function() {
9212 return function jQueryLikeParamSerializer(params) {
9213 if (!params) return '';
9214 var parts = [];
9215 serialize(params, '', true);
9216 return parts.join('&');
9217
9218 function serialize(toSerialize, prefix, topLevel) {
9219 if (toSerialize === null || isUndefined(toSerialize)) return;
9220 if (isArray(toSerialize)) {
9221 forEach(toSerialize, function(value) {
9222 serialize(value, prefix + '[]');
9223 });
9224 } else if (isObject(toSerialize) && !isDate(toSerialize)) {
9225 forEachSorted(toSerialize, function(value, key) {
9226 serialize(value, prefix +
9227 (topLevel ? '' : '[') +
9228 key +
9229 (topLevel ? '' : ']'));
9230 });
9231 } else {
9232 parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize)));
9233 }
9234 }
9235 };
9236 };
9237 }
9238
9239 function defaultHttpResponseTransform(data, headers) {
9240 if (isString(data)) {
9241 // Strip json vulnerability protection prefix and trim whitespace
9242 var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim();
9243
9244 if (tempData) {
9245 var contentType = headers('Content-Type');
9246 if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) {
9247 data = fromJson(tempData);
9248 }
9249 }
9250 }
9251
9252 return data;
9253 }
9254
9255 function isJsonLike(str) {
9256 var jsonStart = str.match(JSON_START);
9257 return jsonStart && JSON_ENDS[jsonStart[0]].test(str);
9258 }
9259
73619260 /**
73629261 * Parse headers into key value object
73639262 *
73659264 * @returns {Object} Parsed headers as key value object
73669265 */
73679266 function parseHeaders(headers) {
7368 var parsed = {}, key, val, i;
7369
7370 if (!headers) return parsed;
7371
7372 forEach(headers.split('\n'), function(line) {
7373 i = line.indexOf(':');
7374 key = lowercase(trim(line.substr(0, i)));
7375 val = trim(line.substr(i + 1));
7376
9267 var parsed = createMap(), i;
9268
9269 function fillInParsed(key, val) {
73779270 if (key) {
73789271 parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
73799272 }
7380 });
9273 }
9274
9275 if (isString(headers)) {
9276 forEach(headers.split('\n'), function(line) {
9277 i = line.indexOf(':');
9278 fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1)));
9279 });
9280 } else if (isObject(headers)) {
9281 forEach(headers, function(headerVal, headerKey) {
9282 fillInParsed(lowercase(headerKey), trim(headerVal));
9283 });
9284 }
73819285
73829286 return parsed;
73839287 }
73969300 * - if called with no arguments returns an object containing all headers.
73979301 */
73989302 function headersGetter(headers) {
7399 var headersObj = isObject(headers) ? headers : undefined;
9303 var headersObj;
74009304
74019305 return function(name) {
74029306 if (!headersObj) headersObj = parseHeaders(headers);
74039307
74049308 if (name) {
7405 return headersObj[lowercase(name)] || null;
9309 var value = headersObj[lowercase(name)];
9310 if (value === void 0) {
9311 value = null;
9312 }
9313 return value;
74069314 }
74079315
74089316 return headersObj;
74169324 * This function is used for both request and response transforming
74179325 *
74189326 * @param {*} data Data to transform.
7419 * @param {function(string=)} headers Http headers getter fn.
9327 * @param {function(string=)} headers HTTP headers getter fn.
9328 * @param {number} status HTTP status code of the response.
74209329 * @param {(Function|Array.<Function>)} fns Function or an array of functions.
74219330 * @returns {*} Transformed data.
74229331 */
7423 function transformData(data, headers, fns) {
7424 if (isFunction(fns))
7425 return fns(data, headers);
9332 function transformData(data, headers, status, fns) {
9333 if (isFunction(fns)) {
9334 return fns(data, headers, status);
9335 }
74269336
74279337 forEach(fns, function(fn) {
7428 data = fn(data, headers);
9338 data = fn(data, headers, status);
74299339 });
74309340
74319341 return data;
74449354 * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
74459355 * */
74469356 function $HttpProvider() {
7447 var JSON_START = /^\s*(\[|\{[^\{])/,
7448 JSON_END = /[\}\]]\s*$/,
7449 PROTECTION_PREFIX = /^\)\]\}',?\n/,
7450 CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'};
7451
74529357 /**
74539358 * @ngdoc property
74549359 * @name $httpProvider#defaults
74559360 * @description
74569361 *
74579362 * Object containing default values for all {@link ng.$http $http} requests.
9363 *
9364 * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`}
9365 * that will provide the cache for all requests who set their `cache` property to `true`.
9366 * If you set the `defaults.cache = false` then only requests that specify their own custom
9367 * cache object will be cached. See {@link $http#caching $http Caching} for more information.
74589368 *
74599369 * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token.
74609370 * Defaults value is `'XSRF-TOKEN'`.
74699379 * - **`defaults.headers.post`**
74709380 * - **`defaults.headers.put`**
74719381 * - **`defaults.headers.patch`**
9382 *
9383 *
9384 * - **`defaults.paramSerializer`** - `{string|function(Object<string,string>):string}` - A function
9385 * used to the prepare string representation of request parameters (specified as an object).
9386 * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
9387 * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
9388 *
74729389 **/
74739390 var defaults = this.defaults = {
74749391 // transform incoming response data
7475 transformResponse: [function(data) {
7476 if (isString(data)) {
7477 // strip json vulnerability protection prefix
7478 data = data.replace(PROTECTION_PREFIX, '');
7479 if (JSON_START.test(data) && JSON_END.test(data))
7480 data = fromJson(data);
7481 }
7482 return data;
7483 }],
9392 transformResponse: [defaultHttpResponseTransform],
74849393
74859394 // transform outgoing request data
74869395 transformRequest: [function(d) {
7487 return isObject(d) && !isFile(d) && !isBlob(d) ? toJson(d) : d;
9396 return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d;
74889397 }],
74899398
74909399 // default headers
74989407 },
74999408
75009409 xsrfCookieName: 'XSRF-TOKEN',
7501 xsrfHeaderName: 'X-XSRF-TOKEN'
9410 xsrfHeaderName: 'X-XSRF-TOKEN',
9411
9412 paramSerializer: '$httpParamSerializer'
75029413 };
75039414
9415 var useApplyAsync = false;
75049416 /**
7505 * Are ordered by request, i.e. they are applied in the same order as the
9417 * @ngdoc method
9418 * @name $httpProvider#useApplyAsync
9419 * @description
9420 *
9421 * Configure $http service to combine processing of multiple http responses received at around
9422 * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in
9423 * significant performance improvement for bigger applications that make many HTTP requests
9424 * concurrently (common during application bootstrap).
9425 *
9426 * Defaults to false. If no value is specified, returns the current configured value.
9427 *
9428 * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred
9429 * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window
9430 * to load and share the same digest cycle.
9431 *
9432 * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
9433 * otherwise, returns the current configured value.
9434 **/
9435 this.useApplyAsync = function(value) {
9436 if (isDefined(value)) {
9437 useApplyAsync = !!value;
9438 return this;
9439 }
9440 return useApplyAsync;
9441 };
9442
9443 /**
9444 * @ngdoc property
9445 * @name $httpProvider#interceptors
9446 * @description
9447 *
9448 * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http}
9449 * pre-processing of request or postprocessing of responses.
9450 *
9451 * These service factories are ordered by request, i.e. they are applied in the same order as the
75069452 * array, on request, but reverse order, on response.
7507 */
9453 *
9454 * {@link ng.$http#interceptors Interceptors detailed info}
9455 **/
75089456 var interceptorFactories = this.interceptors = [];
75099457
7510 /**
7511 * For historical reasons, response interceptors are ordered by the order in which
7512 * they are applied to the response. (This is the opposite of interceptorFactories)
7513 */
7514 var responseInterceptorFactories = this.responseInterceptors = [];
7515
7516 this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector',
7517 function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) {
9458 this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
9459 function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
75189460
75199461 var defaultCache = $cacheFactory('$http');
9462
9463 /**
9464 * Make sure that default param serializer is exposed as a function
9465 */
9466 defaults.paramSerializer = isString(defaults.paramSerializer) ?
9467 $injector.get(defaults.paramSerializer) : defaults.paramSerializer;
75209468
75219469 /**
75229470 * Interceptors stored in reverse order. Inner interceptors before outer interceptors.
75299477 reversedInterceptors.unshift(isString(interceptorFactory)
75309478 ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
75319479 });
7532
7533 forEach(responseInterceptorFactories, function(interceptorFactory, index) {
7534 var responseFn = isString(interceptorFactory)
7535 ? $injector.get(interceptorFactory)
7536 : $injector.invoke(interceptorFactory);
7537
7538 /**
7539 * Response interceptors go before "around" interceptors (no real reason, just
7540 * had to pick one.) But they are already reversed, so we can't use unshift, hence
7541 * the splice.
7542 */
7543 reversedInterceptors.splice(index, 0, {
7544 response: function(response) {
7545 return responseFn($q.when(response));
7546 },
7547 responseError: function(response) {
7548 return responseFn($q.reject(response));
7549 }
7550 });
7551 });
7552
75539480
75549481 /**
75559482 * @ngdoc service
75779504 * it is important to familiarize yourself with these APIs and the guarantees they provide.
75789505 *
75799506 *
7580 * # General usage
9507 * ## General usage
75819508 * The `$http` service is a function which takes a single argument — a configuration object —
75829509 * that is used to generate an HTTP request and returns a {@link ng.$q promise}
75839510 * with two $http specific methods: `success` and `error`.
75849511 *
75859512 * ```js
7586 * $http({method: 'GET', url: '/someUrl'}).
9513 * // Simple GET request example :
9514 * $http.get('/someUrl').
75879515 * success(function(data, status, headers, config) {
75889516 * // this callback will be called asynchronously
75899517 * // when the response is available
75939521 * // or server returns response with an error status.
75949522 * });
75959523 * ```
9524 *
9525 * ```js
9526 * // Simple POST request example (passing data) :
9527 * $http.post('/someUrl', {msg:'hello word!'}).
9528 * success(function(data, status, headers, config) {
9529 * // this callback will be called asynchronously
9530 * // when the response is available
9531 * }).
9532 * error(function(data, status, headers, config) {
9533 * // called asynchronously if an error occurs
9534 * // or server returns response with an error status.
9535 * });
9536 * ```
9537 *
75969538 *
75979539 * Since the returned value of calling the $http function is a `promise`, you can also use
75989540 * the `then` method to register callbacks, and these callbacks will receive a single argument –
76049546 * XMLHttpRequest will transparently follow it, meaning that the error callback will not be
76059547 * called for such responses.
76069548 *
7607 * # Writing Unit Tests that use $http
9549 * ## Writing Unit Tests that use $http
76089550 * When unit testing (using {@link ngMock ngMock}), it is necessary to call
76099551 * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
76109552 * request using trained responses.
76159557 * $httpBackend.flush();
76169558 * ```
76179559 *
7618 * # Shortcut methods
9560 * ## Shortcut methods
76199561 *
76209562 * Shortcut methods are also available. All shortcut methods require passing in the URL, and
76219563 * request data must be passed in for POST/PUT requests.
76369578 * - {@link ng.$http#patch $http.patch}
76379579 *
76389580 *
7639 * # Setting HTTP Headers
9581 * ## Setting HTTP Headers
76409582 *
76419583 * The $http service will automatically add certain HTTP headers to all requests. These defaults
76429584 * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration
76529594 * To add or overwrite these defaults, simply add or remove a property from these configuration
76539595 * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object
76549596 * with the lowercased HTTP method name as the key, e.g.
7655 * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }.
9597 * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`.
76569598 *
76579599 * The defaults can also be set at runtime via the `$http.defaults` object in the same
76589600 * fashion. For example:
76669608 * In addition, you can supply a `headers` property in the config object passed when
76679609 * calling `$http(config)`, which overrides the defaults without changing them globally.
76689610 *
9611 * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis,
9612 * Use the `headers` property, setting the desired header to `undefined`. For example:
76699613 *
7670 * # Transforming Requests and Responses
9614 * ```js
9615 * var req = {
9616 * method: 'POST',
9617 * url: 'http://example.com',
9618 * headers: {
9619 * 'Content-Type': undefined
9620 * },
9621 * data: { test: 'test' }
9622 * }
76719623 *
7672 * Both requests and responses can be transformed using transform functions. By default, Angular
7673 * applies these transformations:
9624 * $http(req).success(function(){...}).error(function(){...});
9625 * ```
76749626 *
7675 * Request transformations:
9627 * ## Transforming Requests and Responses
9628 *
9629 * Both requests and responses can be transformed using transformation functions: `transformRequest`
9630 * and `transformResponse`. These properties can be a single function that returns
9631 * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions,
9632 * which allows you to `push` or `unshift` a new transformation function into the transformation chain.
9633 *
9634 * ### Default Transformations
9635 *
9636 * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and
9637 * `defaults.transformResponse` properties. If a request does not provide its own transformations
9638 * then these will be applied.
9639 *
9640 * You can augment or replace the default transformations by modifying these properties by adding to or
9641 * replacing the array.
9642 *
9643 * Angular provides the following default transformations:
9644 *
9645 * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`):
76769646 *
76779647 * - If the `data` property of the request configuration object contains an object, serialize it
76789648 * into JSON format.
76799649 *
7680 * Response transformations:
9650 * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`):
76819651 *
76829652 * - If XSRF prefix is detected, strip it (see Security Considerations section below).
76839653 * - If JSON response is detected, deserialize it using a JSON parser.
76849654 *
7685 * To globally augment or override the default transforms, modify the
7686 * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse`
7687 * properties. These properties are by default an array of transform functions, which allows you
7688 * to `push` or `unshift` a new transformation function into the transformation chain. You can
7689 * also decide to completely override any default transformations by assigning your
7690 * transformation functions to these properties directly without the array wrapper. These defaults
7691 * are again available on the $http factory at run-time, which may be useful if you have run-time
7692 * services you wish to be involved in your transformations.
76939655 *
7694 * Similarly, to locally override the request/response transforms, augment the
7695 * `transformRequest` and/or `transformResponse` properties of the configuration object passed
9656 * ### Overriding the Default Transformations Per Request
9657 *
9658 * If you wish override the request/response transformations only for a single request then provide
9659 * `transformRequest` and/or `transformResponse` properties on the configuration object passed
76969660 * into `$http`.
76979661 *
9662 * Note that if you provide these properties on the config object the default transformations will be
9663 * overwritten. If you wish to augment the default transformations then you must include them in your
9664 * local transformation array.
76989665 *
7699 * # Caching
9666 * The following code demonstrates adding a new response transformation to be run after the default response
9667 * transformations have been run.
9668 *
9669 * ```js
9670 * function appendTransform(defaults, transform) {
9671 *
9672 * // We can't guarantee that the default transformation is an array
9673 * defaults = angular.isArray(defaults) ? defaults : [defaults];
9674 *
9675 * // Append the new transformation to the defaults
9676 * return defaults.concat(transform);
9677 * }
9678 *
9679 * $http({
9680 * url: '...',
9681 * method: 'GET',
9682 * transformResponse: appendTransform($http.defaults.transformResponse, function(value) {
9683 * return doTransform(value);
9684 * })
9685 * });
9686 * ```
9687 *
9688 *
9689 * ## Caching
77009690 *
77019691 * To enable caching, set the request configuration `cache` property to `true` (to use default
77029692 * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}).
77139703 *
77149704 * You can change the default cache to a new object (built with
77159705 * {@link ng.$cacheFactory `$cacheFactory`}) by updating the
7716 * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set
9706 * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set
77179707 * their `cache` property to `true` will now use this cache object.
77189708 *
77199709 * If you set the default cache to `false` then only requests that specify their own custom
77209710 * cache object will be cached.
77219711 *
7722 * # Interceptors
9712 * ## Interceptors
77239713 *
77249714 * Before you start creating interceptors, be sure to understand the
77259715 * {@link ng.$q $q and deferred/promise APIs}.
78049794 * });
78059795 * ```
78069796 *
7807 * # Response interceptors (DEPRECATED)
7808 *
7809 * Before you start creating interceptors, be sure to understand the
7810 * {@link ng.$q $q and deferred/promise APIs}.
7811 *
7812 * For purposes of global error handling, authentication or any kind of synchronous or
7813 * asynchronous preprocessing of received responses, it is desirable to be able to intercept
7814 * responses for http requests before they are handed over to the application code that
7815 * initiated these requests. The response interceptors leverage the {@link ng.$q
7816 * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing.
7817 *
7818 * The interceptors are service factories that are registered with the $httpProvider by
7819 * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and
7820 * injected with dependencies (if specified) and returns the interceptor — a function that
7821 * takes a {@link ng.$q promise} and returns the original or a new promise.
7822 *
7823 * ```js
7824 * // register the interceptor as a service
7825 * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
7826 * return function(promise) {
7827 * return promise.then(function(response) {
7828 * // do something on success
7829 * return response;
7830 * }, function(response) {
7831 * // do something on error
7832 * if (canRecover(response)) {
7833 * return responseOrNewPromise
7834 * }
7835 * return $q.reject(response);
7836 * });
7837 * }
7838 * });
7839 *
7840 * $httpProvider.responseInterceptors.push('myHttpInterceptor');
7841 *
7842 *
7843 * // register the interceptor via an anonymous factory
7844 * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) {
7845 * return function(promise) {
7846 * // same as above
7847 * }
7848 * });
7849 * ```
7850 *
7851 *
7852 * # Security Considerations
9797 * ## Security Considerations
78539798 *
78549799 * When designing web applications, consider security threats from:
78559800 *
78609805 * pre-configured with strategies that address these issues, but for this to work backend server
78619806 * cooperation is required.
78629807 *
7863 * ## JSON Vulnerability Protection
9808 * ### JSON Vulnerability Protection
78649809 *
78659810 * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx)
78669811 * allows third party website to turn your JSON resource URL into
78829827 * Angular will strip the prefix, before processing the JSON.
78839828 *
78849829 *
7885 * ## Cross Site Request Forgery (XSRF) Protection
9830 * ### Cross Site Request Forgery (XSRF) Protection
78869831 *
78879832 * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which
78889833 * an unauthorized site can gain your user's private data. Angular provides a mechanism
79059850 * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
79069851 * or the per-request config object.
79079852 *
9853 * In order to prevent collisions in environments where multiple Angular apps share the
9854 * same domain or subdomain, we recommend that each application uses unique cookie name.
9855 *
79089856 *
79099857 * @param {object} config Object describing the request to be made and how it should be
79109858 * processed. The object has following properties:
79119859 *
79129860 * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
79139861 * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
7914 * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned
7915 * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be
7916 * JSONified.
9862 * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized
9863 * with the `paramSerializer` and appended as GET parameters.
79179864 * - **data** – `{string|Object}` – Data to be sent as the request message data.
79189865 * - **headers** – `{Object}` – Map of strings or functions which return strings representing
79199866 * HTTP headers to send to the server. If the return value of a function is null, the
7920 * header will not be sent.
9867 * header will not be sent. Functions accept a config object as an argument.
79219868 * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token.
79229869 * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token.
79239870 * - **transformRequest** –
79249871 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
79259872 * transform function or an array of such functions. The transform function takes the http
79269873 * request body and headers and returns its transformed (typically serialized) version.
9874 * See {@link ng.$http#overriding-the-default-transformations-per-request
9875 * Overriding the Default Transformations}
79279876 * - **transformResponse** –
7928 * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
9877 * `{function(data, headersGetter, status)|Array.<function(data, headersGetter, status)>}` –
79299878 * transform function or an array of such functions. The transform function takes the http
7930 * response body and headers and returns its transformed (typically deserialized) version.
9879 * response body, headers and status and returns its transformed (typically deserialized) version.
9880 * See {@link ng.$http#overriding-the-default-transformations-per-request
9881 * Overriding the Default TransformationjqLiks}
9882 * - **paramSerializer** - `{string|function(Object<string,string>):string}` - A function used to
9883 * prepare the string representation of request parameters (specified as an object).
9884 * If specified as string, it is interpreted as function registered with the
9885 * {@link $injector $injector}, which means you can create your own serializer
9886 * by registering it as a {@link auto.$provide#service service}.
9887 * The default serializer is the {@link $httpParamSerializer $httpParamSerializer};
9888 * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike}
79319889 * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
79329890 * GET request, otherwise if a cache instance built with
79339891 * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
79389896 * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials)
79399897 * for more information.
79409898 * - **responseType** - `{string}` - see
7941 * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
9899 * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype).
79429900 *
79439901 * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the
79449902 * standard `then` method and two http specific methods: `success` and `error`. The `then`
79639921 <example module="httpExample">
79649922 <file name="index.html">
79659923 <div ng-controller="FetchController">
7966 <select ng-model="method">
9924 <select ng-model="method" aria-label="Request method">
79679925 <option>GET</option>
79689926 <option>JSONP</option>
79699927 </select>
7970 <input type="text" ng-model="url" size="80"/>
9928 <input type="text" ng-model="url" size="80" aria-label="URL" />
79719929 <button id="fetchbtn" ng-click="fetch()">fetch</button><br>
79729930 <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
79739931 <button id="samplejsonpbtn"
80299987 expect(data.getText()).toMatch(/Hello, \$http!/);
80309988 });
80319989
8032 it('should make a JSONP request to angularjs.org', function() {
8033 sampleJsonpBtn.click();
8034 fetchBtn.click();
8035 expect(status.getText()).toMatch('200');
8036 expect(data.getText()).toMatch(/Super Hero!/);
8037 });
9990 // Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185
9991 // it('should make a JSONP request to angularjs.org', function() {
9992 // sampleJsonpBtn.click();
9993 // fetchBtn.click();
9994 // expect(status.getText()).toMatch('200');
9995 // expect(data.getText()).toMatch(/Super Hero!/);
9996 // });
80389997
80399998 it('should make JSONP request to invalid URL and invoke the error handler',
80409999 function() {
804710006 </example>
804810007 */
804910008 function $http(requestConfig) {
8050 var config = {
10009
10010 if (!angular.isObject(requestConfig)) {
10011 throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
10012 }
10013
10014 var config = extend({
805110015 method: 'get',
805210016 transformRequest: defaults.transformRequest,
8053 transformResponse: defaults.transformResponse
8054 };
8055 var headers = mergeHeaders(requestConfig);
8056
8057 extend(config, requestConfig);
8058 config.headers = headers;
10017 transformResponse: defaults.transformResponse,
10018 paramSerializer: defaults.paramSerializer
10019 }, requestConfig);
10020
10021 config.headers = mergeHeaders(requestConfig);
805910022 config.method = uppercase(config.method);
10023 config.paramSerializer = isString(config.paramSerializer) ?
10024 $injector.get(config.paramSerializer) : config.paramSerializer;
806010025
806110026 var serverRequest = function(config) {
8062 headers = config.headers;
8063 var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);
10027 var headers = config.headers;
10028 var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest);
806410029
806510030 // strip content-type if data is undefined
806610031 if (isUndefined(reqData)) {
807610041 }
807710042
807810043 // send request
8079 return sendReq(config, reqData, headers).then(transformResponse, transformResponse);
10044 return sendReq(config, reqData).then(transformResponse, transformResponse);
808010045 };
808110046
808210047 var chain = [serverRequest, undefined];
809210057 }
809310058 });
809410059
8095 while(chain.length) {
10060 while (chain.length) {
809610061 var thenFn = chain.shift();
809710062 var rejectFn = chain.shift();
809810063
810010065 }
810110066
810210067 promise.success = function(fn) {
10068 assertArgFn(fn, 'fn');
10069
810310070 promise.then(function(response) {
810410071 fn(response.data, response.status, response.headers, config);
810510072 });
810710074 };
810810075
810910076 promise.error = function(fn) {
10077 assertArgFn(fn, 'fn');
10078
811010079 promise.then(null, function(response) {
811110080 fn(response.data, response.status, response.headers, config);
811210081 });
811710086
811810087 function transformResponse(response) {
811910088 // make a copy since the response must be cacheable
8120 var resp = extend({}, response, {
8121 data: transformData(response.data, response.headers, config.transformResponse)
8122 });
10089 var resp = extend({}, response);
10090 if (!response.data) {
10091 resp.data = response.data;
10092 } else {
10093 resp.data = transformData(response.data, response.headers, response.status, config.transformResponse);
10094 }
812310095 return (isSuccess(response.status))
812410096 ? resp
812510097 : $q.reject(resp);
10098 }
10099
10100 function executeHeaderFns(headers, config) {
10101 var headerContent, processedHeaders = {};
10102
10103 forEach(headers, function(headerFn, header) {
10104 if (isFunction(headerFn)) {
10105 headerContent = headerFn(config);
10106 if (headerContent != null) {
10107 processedHeaders[header] = headerContent;
10108 }
10109 } else {
10110 processedHeaders[header] = headerFn;
10111 }
10112 });
10113
10114 return processedHeaders;
812610115 }
812710116
812810117 function mergeHeaders(config) {
814710136 }
814810137
814910138 // execute if header value is a function for merged headers
8150 execHeaders(reqHeaders);
8151 return reqHeaders;
8152
8153 function execHeaders(headers) {
8154 var headerContent;
8155
8156 forEach(headers, function(headerFn, header) {
8157 if (isFunction(headerFn)) {
8158 headerContent = headerFn();
8159 if (headerContent != null) {
8160 headers[header] = headerContent;
8161 } else {
8162 delete headers[header];
8163 }
8164 }
8165 });
8166 }
10139 return executeHeaderFns(reqHeaders, shallowCopy(config));
816710140 }
816810141 }
816910142
824410217 * @param {Object=} config Optional configuration object
824510218 * @returns {HttpPromise} Future object
824610219 */
8247 createShortMethodsWithData('post', 'put');
10220
10221 /**
10222 * @ngdoc method
10223 * @name $http#patch
10224 *
10225 * @description
10226 * Shortcut method to perform `PATCH` request.
10227 *
10228 * @param {string} url Relative or absolute URL specifying the destination of the request
10229 * @param {*} data Request content
10230 * @param {Object=} config Optional configuration object
10231 * @returns {HttpPromise} Future object
10232 */
10233 createShortMethodsWithData('post', 'put', 'patch');
824810234
824910235 /**
825010236 * @ngdoc property
826510251 function createShortMethods(names) {
826610252 forEach(arguments, function(name) {
826710253 $http[name] = function(url, config) {
8268 return $http(extend(config || {}, {
10254 return $http(extend({}, config || {}, {
826910255 method: name,
827010256 url: url
827110257 }));
827710263 function createShortMethodsWithData(name) {
827810264 forEach(arguments, function(name) {
827910265 $http[name] = function(url, data, config) {
8280 return $http(extend(config || {}, {
10266 return $http(extend({}, config || {}, {
828110267 method: name,
828210268 url: url,
828310269 data: data
829310279 * !!! ACCESSES CLOSURE VARS:
829410280 * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests
829510281 */
8296 function sendReq(config, reqData, reqHeaders) {
10282 function sendReq(config, reqData) {
829710283 var deferred = $q.defer(),
829810284 promise = deferred.promise,
829910285 cache,
830010286 cachedResp,
8301 url = buildUrl(config.url, config.params);
10287 reqHeaders = config.headers,
10288 url = buildUrl(config.url, config.paramSerializer(config.params));
830210289
830310290 $http.pendingRequests.push(config);
830410291 promise.then(removePendingReq, removePendingReq);
831610303 if (isDefined(cachedResp)) {
831710304 if (isPromiseLike(cachedResp)) {
831810305 // cached request has already been sent, but there is no response yet
8319 cachedResp.then(removePendingReq, removePendingReq);
8320 return cachedResp;
10306 cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult);
832110307 } else {
832210308 // serving from cache
832310309 if (isArray(cachedResp)) {
833710323 // send the request to the backend
833810324 if (isUndefined(cachedResp)) {
833910325 var xsrfValue = urlIsSameOrigin(config.url)
8340 ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
10326 ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
834110327 : undefined;
834210328 if (xsrfValue) {
834310329 reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
836610352 }
836710353 }
836810354
8369 resolvePromise(response, status, headersString, statusText);
8370 if (!$rootScope.$$phase) $rootScope.$apply();
10355 function resolveHttpPromise() {
10356 resolvePromise(response, status, headersString, statusText);
10357 }
10358
10359 if (useApplyAsync) {
10360 $rootScope.$applyAsync(resolveHttpPromise);
10361 } else {
10362 resolveHttpPromise();
10363 if (!$rootScope.$$phase) $rootScope.$apply();
10364 }
837110365 }
837210366
837310367
838310377 status: status,
838410378 headers: headersGetter(headers),
838510379 config: config,
8386 statusText : statusText
10380 statusText: statusText
838710381 });
838810382 }
838910383
10384 function resolvePromiseWithResult(result) {
10385 resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText);
10386 }
839010387
839110388 function removePendingReq() {
8392 var idx = indexOf($http.pendingRequests, config);
10389 var idx = $http.pendingRequests.indexOf(config);
839310390 if (idx !== -1) $http.pendingRequests.splice(idx, 1);
839410391 }
839510392 }
839610393
839710394
8398 function buildUrl(url, params) {
8399 if (!params) return url;
8400 var parts = [];
8401 forEachSorted(params, function(value, key) {
8402 if (value === null || isUndefined(value)) return;
8403 if (!isArray(value)) value = [value];
8404
8405 forEach(value, function(v) {
8406 if (isObject(v)) {
8407 if (isDate(v)){
8408 v = v.toISOString();
8409 } else if (isObject(v)) {
8410 v = toJson(v);
8411 }
8412 }
8413 parts.push(encodeUriQuery(key) + '=' +
8414 encodeUriQuery(v));
8415 });
8416 });
8417 if(parts.length > 0) {
8418 url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&');
10395 function buildUrl(url, serializedParams) {
10396 if (serializedParams.length > 0) {
10397 url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams;
841910398 }
842010399 return url;
842110400 }
842210401 }];
842310402 }
842410403
8425 function createXhr(method) {
8426 //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest
8427 //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest
8428 //if it is available
8429 if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) ||
8430 !window.XMLHttpRequest)) {
8431 return new window.ActiveXObject("Microsoft.XMLHTTP");
8432 } else if (window.XMLHttpRequest) {
8433 return new window.XMLHttpRequest();
8434 }
8435
8436 throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest.");
10404 function createXhr() {
10405 return new window.XMLHttpRequest();
843710406 }
843810407
843910408 /**
845910428 }
846010429
846110430 function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
8462 var ABORTED = -1;
8463
846410431 // TODO(vojta): fix the signature
846510432 return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
8466 var status;
846710433 $browser.$$incOutstandingRequestCount();
846810434 url = url || $browser.url();
846910435
848110447 });
848210448 } else {
848310449
8484 var xhr = createXhr(method);
10450 var xhr = createXhr();
848510451
848610452 xhr.open(method, url, true);
848710453 forEach(headers, function(value, key) {
849010456 }
849110457 });
849210458
8493 // In IE6 and 7, this might be called synchronously when xhr.send below is called and the
8494 // response is in the cache. the promise api will ensure that to the app code the api is
8495 // always async
8496 xhr.onreadystatechange = function() {
8497 // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by
8498 // xhrs that are resolved while the app is in the background (see #5426).
8499 // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before
8500 // continuing
8501 //
8502 // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and
8503 // Safari respectively.
8504 if (xhr && xhr.readyState == 4) {
8505 var responseHeaders = null,
8506 response = null,
8507 statusText = '';
8508
8509 if(status !== ABORTED) {
8510 responseHeaders = xhr.getAllResponseHeaders();
8511
8512 // responseText is the old-school way of retrieving response (supported by IE8 & 9)
8513 // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
8514 response = ('response' in xhr) ? xhr.response : xhr.responseText;
8515 }
8516
8517 // Accessing statusText on an aborted xhr object will
8518 // throw an 'c00c023f error' in IE9 and lower, don't touch it.
8519 if (!(status === ABORTED && msie < 10)) {
8520 statusText = xhr.statusText;
8521 }
8522
8523 completeRequest(callback,
8524 status || xhr.status,
8525 response,
8526 responseHeaders,
8527 statusText);
10459 xhr.onload = function requestLoaded() {
10460 var statusText = xhr.statusText || '';
10461
10462 // responseText is the old-school way of retrieving response (supported by IE8 & 9)
10463 // response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
10464 var response = ('response' in xhr) ? xhr.response : xhr.responseText;
10465
10466 // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
10467 var status = xhr.status === 1223 ? 204 : xhr.status;
10468
10469 // fix status code when it is 0 (0 status is undocumented).
10470 // Occurs when accessing file resources or on Android 4.1 stock browser
10471 // while retrieving files from application cache.
10472 if (status === 0) {
10473 status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0;
852810474 }
10475
10476 completeRequest(callback,
10477 status,
10478 response,
10479 xhr.getAllResponseHeaders(),
10480 statusText);
852910481 };
10482
10483 var requestError = function() {
10484 // The response is always empty
10485 // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
10486 completeRequest(callback, -1, null, null, '');
10487 };
10488
10489 xhr.onerror = requestError;
10490 xhr.onabort = requestError;
853010491
853110492 if (withCredentials) {
853210493 xhr.withCredentials = true;
854910510 }
855010511 }
855110512
8552 xhr.send(post || null);
10513 xhr.send(post);
855310514 }
855410515
855510516 if (timeout > 0) {
856010521
856110522
856210523 function timeoutRequest() {
8563 status = ABORTED;
856410524 jsonpDone && jsonpDone();
856510525 xhr && xhr.abort();
856610526 }
856710527
856810528 function completeRequest(callback, status, response, headersString, statusText) {
856910529 // cancel timeout and subsequent timeout promise resolution
8570 timeoutId && $browserDefer.cancel(timeoutId);
10530 if (timeoutId !== undefined) {
10531 $browserDefer.cancel(timeoutId);
10532 }
857110533 jsonpDone = xhr = null;
8572
8573 // fix status code when it is 0 (0 status is undocumented).
8574 // Occurs when accessing file resources or on Android 4.1 stock browser
8575 // while retrieving files from application cache.
8576 if (status === 0) {
8577 status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0;
8578 }
8579
8580 // normalize IE bug (http://bugs.jquery.com/ticket/1450)
8581 status = status === 1223 ? 204 : status;
8582 statusText = statusText || '';
858310534
858410535 callback(status, response, headersString, statusText);
858510536 $browser.$$completeOutstandingRequest(noop);
858710538 };
858810539
858910540 function jsonpReq(url, callbackId, done) {
8590 // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.:
10541 // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.:
859110542 // - fetches local scripts via XHR and evals them
859210543 // - adds and immediately removes script elements from the document
859310544 var script = rawDocument.createElement('script'), callback = null;
861810569
861910570 addEventListenerFn(script, "load", callback);
862010571 addEventListenerFn(script, "error", callback);
8621
8622 if (msie <= 8) {
8623 script.onreadystatechange = function() {
8624 if (isString(script.readyState) && /loaded|complete/.test(script.readyState)) {
8625 script.onreadystatechange = null;
8626 callback({
8627 type: 'load'
8628 });
8629 }
8630 };
8631 }
8632
863310572 rawDocument.body.appendChild(script);
863410573 return callback;
863510574 }
863610575 }
863710576
8638 var $interpolateMinErr = minErr('$interpolate');
10577 var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate');
10578 $interpolateMinErr.throwNoconcat = function(text) {
10579 throw $interpolateMinErr('noconcat',
10580 "Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
10581 "interpolations that concatenate multiple expressions when a trusted value is " +
10582 "required. See http://docs.angularjs.org/api/ng.$sce", text);
10583 };
10584
10585 $interpolateMinErr.interr = function(text, err) {
10586 return $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString());
10587 };
863910588
864010589 /**
864110590 * @ngdoc provider
864210591 * @name $interpolateProvider
8643 * @kind function
864410592 *
864510593 * @description
864610594 *
868610634 * @param {string=} value new value to set the starting symbol to.
868710635 * @returns {string|self} Returns the symbol when used as getter and self if used as setter.
868810636 */
8689 this.startSymbol = function(value){
10637 this.startSymbol = function(value) {
869010638 if (value) {
869110639 startSymbol = value;
869210640 return this;
870410652 * @param {string=} value new value to set the ending symbol to.
870510653 * @returns {string|self} Returns the symbol when used as getter and self if used as setter.
870610654 */
8707 this.endSymbol = function(value){
10655 this.endSymbol = function(value) {
870810656 if (value) {
870910657 endSymbol = value;
871010658 return this;
871610664
871710665 this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) {
871810666 var startSymbolLength = startSymbol.length,
8719 endSymbolLength = endSymbol.length;
10667 endSymbolLength = endSymbol.length,
10668 escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'),
10669 escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g');
10670
10671 function escape(ch) {
10672 return '\\\\\\' + ch;
10673 }
10674
10675 function unescapeText(text) {
10676 return text.replace(escapedStartRegexp, startSymbol).
10677 replace(escapedEndRegexp, endSymbol);
10678 }
10679
10680 function stringify(value) {
10681 if (value == null) { // null || undefined
10682 return '';
10683 }
10684 switch (typeof value) {
10685 case 'string':
10686 break;
10687 case 'number':
10688 value = '' + value;
10689 break;
10690 default:
10691 value = toJson(value);
10692 }
10693
10694 return value;
10695 }
872010696
872110697 /**
872210698 * @ngdoc service
874010716 * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!');
874110717 * ```
874210718 *
10719 * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is
10720 * `true`, the interpolation function will return `undefined` unless all embedded expressions
10721 * evaluate to a value other than `undefined`.
10722 *
10723 * ```js
10724 * var $interpolate = ...; // injected
10725 * var context = {greeting: 'Hello', name: undefined };
10726 *
10727 * // default "forgiving" mode
10728 * var exp = $interpolate('{{greeting}} {{name}}!');
10729 * expect(exp(context)).toEqual('Hello !');
10730 *
10731 * // "allOrNothing" mode
10732 * exp = $interpolate('{{greeting}} {{name}}!', false, null, true);
10733 * expect(exp(context)).toBeUndefined();
10734 * context.name = 'Angular';
10735 * expect(exp(context)).toEqual('Hello Angular!');
10736 * ```
10737 *
10738 * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior.
10739 *
10740 * ####Escaped Interpolation
10741 * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers
10742 * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash).
10743 * It will be rendered as a regular start/end marker, and will not be interpreted as an expression
10744 * or binding.
10745 *
10746 * This enables web-servers to prevent script injection attacks and defacing attacks, to some
10747 * degree, while also enabling code examples to work without relying on the
10748 * {@link ng.directive:ngNonBindable ngNonBindable} directive.
10749 *
10750 * **For security purposes, it is strongly encouraged that web servers escape user-supplied data,
10751 * replacing angle brackets (&lt;, &gt;) with &amp;lt; and &amp;gt; respectively, and replacing all
10752 * interpolation start/end markers with their escaped counterparts.**
10753 *
10754 * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered
10755 * output when the $interpolate service processes the text. So, for HTML elements interpolated
10756 * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter
10757 * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such,
10758 * this is typically useful only when user-data is used in rendering a template from the server, or
10759 * when otherwise untrusted data is used by a directive.
10760 *
10761 * <example>
10762 * <file name="index.html">
10763 * <div ng-init="username='A user'">
10764 * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\}
10765 * </p>
10766 * <p><strong>{{username}}</strong> attempts to inject code which will deface the
10767 * application, but fails to accomplish their task, because the server has correctly
10768 * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash)
10769 * characters.</p>
10770 * <p>Instead, the result of the attempted script injection is visible, and can be removed
10771 * from the database by an administrator.</p>
10772 * </div>
10773 * </file>
10774 * </example>
874310775 *
874410776 * @param {string} text The text with markup to interpolate.
874510777 * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have
874910781 * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult,
875010782 * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that
875110783 * provides Strict Contextual Escaping for details.
10784 * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined
10785 * unless all embedded expressions evaluate to a value other than `undefined`.
875210786 * @returns {function(context)} an interpolation function which is used to compute the
875310787 * interpolated string. The function has these parameters:
875410788 *
8755 * * `context`: an object against which any expressions embedded in the strings are evaluated
8756 * against.
8757 *
10789 * - `context`: evaluation context for all expressions embedded in the interpolated text
875810790 */
8759 function $interpolate(text, mustHaveExpression, trustedContext) {
10791 function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
10792 allOrNothing = !!allOrNothing;
876010793 var startIndex,
876110794 endIndex,
876210795 index = 0,
8763 parts = [],
8764 length = text.length,
8765 hasInterpolation = false,
8766 fn,
10796 expressions = [],
10797 parseFns = [],
10798 textLength = text.length,
876710799 exp,
8768 concat = [];
8769
8770 while(index < length) {
8771 if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) &&
8772 ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) {
8773 (index != startIndex) && parts.push(text.substring(index, startIndex));
8774 parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex)));
8775 fn.exp = exp;
10800 concat = [],
10801 expressionPositions = [];
10802
10803 while (index < textLength) {
10804 if (((startIndex = text.indexOf(startSymbol, index)) != -1) &&
10805 ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) {
10806 if (index !== startIndex) {
10807 concat.push(unescapeText(text.substring(index, startIndex)));
10808 }
10809 exp = text.substring(startIndex + startSymbolLength, endIndex);
10810 expressions.push(exp);
10811 parseFns.push($parse(exp, parseStringifyInterceptor));
877610812 index = endIndex + endSymbolLength;
8777 hasInterpolation = true;
10813 expressionPositions.push(concat.length);
10814 concat.push('');
877810815 } else {
8779 // we did not find anything, so we have to add the remainder to the parts array
8780 (index != length) && parts.push(text.substring(index));
8781 index = length;
10816 // we did not find an interpolation, so we have to add the remainder to the separators array
10817 if (index !== textLength) {
10818 concat.push(unescapeText(text.substring(index)));
10819 }
10820 break;
878210821 }
8783 }
8784
8785 if (!(length = parts.length)) {
8786 // we added, nothing, must have been an empty string.
8787 parts.push('');
8788 length = 1;
878910822 }
879010823
879110824 // Concatenating expressions makes it hard to reason about whether some combination of
879410827 // that's used is assigned or constructed by some JS code somewhere that is more testable or
879510828 // make it obvious that you bound the value to some user controlled value. This helps reduce
879610829 // the load when auditing for XSS issues.
8797 if (trustedContext && parts.length > 1) {
8798 throw $interpolateMinErr('noconcat',
8799 "Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
8800 "interpolations that concatenate multiple expressions when a trusted value is " +
8801 "required. See http://docs.angularjs.org/api/ng.$sce", text);
8802 }
8803
8804 if (!mustHaveExpression || hasInterpolation) {
8805 concat.length = length;
8806 fn = function(context) {
8807 try {
8808 for(var i = 0, ii = length, part; i<ii; i++) {
8809 if (typeof (part = parts[i]) == 'function') {
8810 part = part(context);
8811 if (trustedContext) {
8812 part = $sce.getTrusted(trustedContext, part);
8813 } else {
8814 part = $sce.valueOf(part);
8815 }
8816 if (part == null) { // null || undefined
8817 part = '';
8818 } else {
8819 switch (typeof part) {
8820 case 'string':
8821 {
8822 break;
8823 }
8824 case 'number':
8825 {
8826 part = '' + part;
8827 break;
8828 }
8829 default:
8830 {
8831 part = toJson(part);
8832 }
8833 }
8834 }
10830 if (trustedContext && concat.length > 1) {
10831 $interpolateMinErr.throwNoconcat(text);
10832 }
10833
10834 if (!mustHaveExpression || expressions.length) {
10835 var compute = function(values) {
10836 for (var i = 0, ii = expressions.length; i < ii; i++) {
10837 if (allOrNothing && isUndefined(values[i])) return;
10838 concat[expressionPositions[i]] = values[i];
10839 }
10840 return concat.join('');
10841 };
10842
10843 var getValue = function(value) {
10844 return trustedContext ?
10845 $sce.getTrusted(trustedContext, value) :
10846 $sce.valueOf(value);
10847 };
10848
10849 return extend(function interpolationFn(context) {
10850 var i = 0;
10851 var ii = expressions.length;
10852 var values = new Array(ii);
10853
10854 try {
10855 for (; i < ii; i++) {
10856 values[i] = parseFns[i](context);
883510857 }
8836 concat[i] = part;
10858
10859 return compute(values);
10860 } catch (err) {
10861 $exceptionHandler($interpolateMinErr.interr(text, err));
883710862 }
8838 return concat.join('');
10863
10864 }, {
10865 // all of these properties are undocumented for now
10866 exp: text, //just for compatibility with regular watchers created via $watch
10867 expressions: expressions,
10868 $$watchDelegate: function(scope, listener) {
10869 var lastValue;
10870 return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
10871 var currValue = compute(values);
10872 if (isFunction(listener)) {
10873 listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
10874 }
10875 lastValue = currValue;
10876 });
883910877 }
8840 catch(err) {
8841 var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
8842 err.toString());
8843 $exceptionHandler(newErr);
8844 }
8845 };
8846 fn.exp = text;
8847 fn.parts = parts;
8848 return fn;
10878 });
10879 }
10880
10881 function parseStringifyInterceptor(value) {
10882 try {
10883 value = getValue(value);
10884 return allOrNothing && !isDefined(value) ? value : stringify(value);
10885 } catch (err) {
10886 $exceptionHandler($interpolateMinErr.interr(text, err));
10887 }
884910888 }
885010889 }
885110890
885610895 * @description
885710896 * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`.
885810897 *
8859 * Use {@link ng.$interpolateProvider#startSymbol $interpolateProvider#startSymbol} to change
10898 * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change
886010899 * the symbol.
886110900 *
886210901 * @returns {string} start symbol.
887210911 * @description
887310912 * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`.
887410913 *
8875 * Use {@link ng.$interpolateProvider#endSymbol $interpolateProvider#endSymbol} to change
10914 * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change
887610915 * the symbol.
887710916 *
887810917 * @returns {string} end symbol.
888610925 }
888710926
888810927 function $IntervalProvider() {
8889 this.$get = ['$rootScope', '$window', '$q',
8890 function($rootScope, $window, $q) {
10928 this.$get = ['$rootScope', '$window', '$q', '$$q',
10929 function($rootScope, $window, $q, $$q) {
889110930 var intervals = {};
889210931
889310932
892310962 * indefinitely.
892410963 * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
892510964 * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
10965 * @param {...*=} Pass additional parameters to the executed function.
892610966 * @returns {promise} A promise which will be notified on each iteration.
892710967 *
892810968 * @example
894110981 * // Don't start a new fight if we are already fighting
894210982 * if ( angular.isDefined(stop) ) return;
894310983 *
8944 * stop = $interval(function() {
8945 * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) {
8946 * $scope.blood_1 = $scope.blood_1 - 3;
8947 * $scope.blood_2 = $scope.blood_2 - 4;
8948 * } else {
8949 * $scope.stopFight();
10984 * stop = $interval(function() {
10985 * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) {
10986 * $scope.blood_1 = $scope.blood_1 - 3;
10987 * $scope.blood_2 = $scope.blood_2 - 4;
10988 * } else {
10989 * $scope.stopFight();
10990 * }
10991 * }, 100);
10992 * };
10993 *
10994 * $scope.stopFight = function() {
10995 * if (angular.isDefined(stop)) {
10996 * $interval.cancel(stop);
10997 * stop = undefined;
895010998 * }
8951 * }, 100);
8952 * };
10999 * };
895311000 *
8954 * $scope.stopFight = function() {
8955 * if (angular.isDefined(stop)) {
8956 * $interval.cancel(stop);
8957 * stop = undefined;
8958 * }
8959 * };
11001 * $scope.resetFight = function() {
11002 * $scope.blood_1 = 100;
11003 * $scope.blood_2 = 120;
11004 * };
896011005 *
8961 * $scope.resetFight = function() {
8962 * $scope.blood_1 = 100;
8963 * $scope.blood_2 = 120;
8964 * };
8965 *
8966 * $scope.$on('$destroy', function() {
8967 * // Make sure that the interval is destroyed too
8968 * $scope.stopFight();
8969 * });
8970 * }])
11006 * $scope.$on('$destroy', function() {
11007 * // Make sure that the interval is destroyed too
11008 * $scope.stopFight();
11009 * });
11010 * }])
897111011 * // Register the 'myCurrentTime' directive factory method.
897211012 * // We inject $interval and dateFilter service since the factory method is DI.
897311013 * .directive('myCurrentTime', ['$interval', 'dateFilter',
899211032 *
899311033 * // listen on DOM destroy (removal) event, and cancel the next UI update
899411034 * // to prevent updating time after the DOM element was removed.
8995 * element.bind('$destroy', function() {
11035 * element.on('$destroy', function() {
899611036 * $interval.cancel(stopTime);
899711037 * });
899811038 * }
900111041 *
900211042 * <div>
900311043 * <div ng-controller="ExampleController">
9004 * Date format: <input ng-model="format"> <hr/>
11044 * <label>Date format: <input ng-model="format"></label> <hr/>
900511045 * Current time is: <span my-current-time="format"></span>
900611046 * <hr/>
900711047 * Blood 1 : <font color='red'>{{blood_1}}</font>
901611056 * </example>
901711057 */
901811058 function interval(fn, delay, count, invokeApply) {
9019 var setInterval = $window.setInterval,
11059 var hasParams = arguments.length > 4,
11060 args = hasParams ? sliceArgs(arguments, 4) : [],
11061 setInterval = $window.setInterval,
902011062 clearInterval = $window.clearInterval,
9021 deferred = $q.defer(),
9022 promise = deferred.promise,
902311063 iteration = 0,
9024 skipApply = (isDefined(invokeApply) && !invokeApply);
11064 skipApply = (isDefined(invokeApply) && !invokeApply),
11065 deferred = (skipApply ? $$q : $q).defer(),
11066 promise = deferred.promise;
902511067
902611068 count = isDefined(count) ? count : 0;
902711069
9028 promise.then(null, null, fn);
11070 promise.then(null, null, (!hasParams) ? fn : function() {
11071 fn.apply(null, args);
11072 });
902911073
903011074 promise.$$intervalId = setInterval(function tick() {
903111075 deferred.notify(iteration++);
908011124 *
908111125 * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
908211126 */
9083 function $LocaleProvider(){
11127 function $LocaleProvider() {
908411128 this.$get = function() {
908511129 return {
908611130 id: 'en-us',
912311167 SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','),
912411168 AMPMS: ['AM','PM'],
912511169 medium: 'MMM d, y h:mm:ss a',
9126 short: 'M/d/yy h:mm a',
11170 'short': 'M/d/yy h:mm a',
912711171 fullDate: 'EEEE, MMMM d, y',
912811172 longDate: 'MMMM d, y',
912911173 mediumDate: 'MMM d, y',
913011174 shortDate: 'M/d/yy',
913111175 mediumTime: 'h:mm:ss a',
9132 shortTime: 'h:mm a'
11176 shortTime: 'h:mm a',
11177 ERANAMES: [
11178 "Before Christ",
11179 "Anno Domini"
11180 ],
11181 ERAS: [
11182 "BC",
11183 "AD"
11184 ]
913311185 },
913411186
913511187 pluralCat: function(num) {
916411216 return segments.join('/');
916511217 }
916611218
9167 function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) {
9168 var parsedUrl = urlResolve(absoluteUrl, appBase);
11219 function parseAbsoluteUrl(absoluteUrl, locationObj) {
11220 var parsedUrl = urlResolve(absoluteUrl);
916911221
917011222 locationObj.$$protocol = parsedUrl.protocol;
917111223 locationObj.$$host = parsedUrl.hostname;
9172 locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
11224 locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
917311225 }
917411226
917511227
9176 function parseAppUrl(relativeUrl, locationObj, appBase) {
11228 function parseAppUrl(relativeUrl, locationObj) {
917711229 var prefixed = (relativeUrl.charAt(0) !== '/');
917811230 if (prefixed) {
917911231 relativeUrl = '/' + relativeUrl;
918011232 }
9181 var match = urlResolve(relativeUrl, appBase);
11233 var match = urlResolve(relativeUrl);
918211234 locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ?
918311235 match.pathname.substring(1) : match.pathname);
918411236 locationObj.$$search = parseKeyValue(match.search);
921011262 return index == -1 ? url : url.substr(0, index);
921111263 }
921211264
11265 function trimEmptyHash(url) {
11266 return url.replace(/(#.+)|#$/, '$1');
11267 }
11268
921311269
921411270 function stripFile(url) {
921511271 return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
923311289 this.$$html5 = true;
923411290 basePrefix = basePrefix || '';
923511291 var appBaseNoFile = stripFile(appBase);
9236 parseAbsoluteUrl(appBase, this, appBase);
11292 parseAbsoluteUrl(appBase, this);
923711293
923811294
923911295 /**
924011296 * Parse given html5 (regular) url string into properties
9241 * @param {string} newAbsoluteUrl HTML5 url
11297 * @param {string} url HTML5 url
924211298 * @private
924311299 */
924411300 this.$$parse = function(url) {
924811304 appBaseNoFile);
924911305 }
925011306
9251 parseAppUrl(pathUrl, this, appBase);
11307 parseAppUrl(pathUrl, this);
925211308
925311309 if (!this.$$path) {
925411310 this.$$path = '/';
926911325 this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
927011326 };
927111327
9272 this.$$rewrite = function(url) {
11328 this.$$parseLinkUrl = function(url, relHref) {
11329 if (relHref && relHref[0] === '#') {
11330 // special case for links to hash fragments:
11331 // keep the old url and only replace the hash fragment
11332 this.hash(relHref.slice(1));
11333 return true;
11334 }
927311335 var appUrl, prevAppUrl;
9274
9275 if ( (appUrl = beginsWith(appBase, url)) !== undefined ) {
11336 var rewrittenUrl;
11337
11338 if ((appUrl = beginsWith(appBase, url)) !== undefined) {
927611339 prevAppUrl = appUrl;
9277 if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) {
9278 return appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
11340 if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) {
11341 rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
927911342 } else {
9280 return appBase + prevAppUrl;
9281 }
9282 } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) {
9283 return appBaseNoFile + appUrl;
11343 rewrittenUrl = appBase + prevAppUrl;
11344 }
11345 } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) {
11346 rewrittenUrl = appBaseNoFile + appUrl;
928411347 } else if (appBaseNoFile == url + '/') {
9285 return appBaseNoFile;
9286 }
11348 rewrittenUrl = appBaseNoFile;
11349 }
11350 if (rewrittenUrl) {
11351 this.$$parse(rewrittenUrl);
11352 }
11353 return !!rewrittenUrl;
928711354 };
928811355 }
928911356
930011367 function LocationHashbangUrl(appBase, hashPrefix) {
930111368 var appBaseNoFile = stripFile(appBase);
930211369
9303 parseAbsoluteUrl(appBase, this, appBase);
11370 parseAbsoluteUrl(appBase, this);
930411371
930511372
930611373 /**
931011377 */
931111378 this.$$parse = function(url) {
931211379 var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url);
9313 var withoutHashUrl = withoutBaseUrl.charAt(0) == '#'
9314 ? beginsWith(hashPrefix, withoutBaseUrl)
9315 : (this.$$html5)
9316 ? withoutBaseUrl
9317 : '';
9318
9319 if (!isString(withoutHashUrl)) {
9320 throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url,
9321 hashPrefix);
9322 }
9323 parseAppUrl(withoutHashUrl, this, appBase);
11380 var withoutHashUrl;
11381
11382 if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') {
11383
11384 // The rest of the url starts with a hash so we have
11385 // got either a hashbang path or a plain hash fragment
11386 withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl);
11387 if (isUndefined(withoutHashUrl)) {
11388 // There was no hashbang prefix so we just have a hash fragment
11389 withoutHashUrl = withoutBaseUrl;
11390 }
11391
11392 } else {
11393 // There was no hashbang path nor hash fragment:
11394 // If we are in HTML5 mode we use what is left as the path;
11395 // Otherwise we ignore what is left
11396 if (this.$$html5) {
11397 withoutHashUrl = withoutBaseUrl;
11398 } else {
11399 withoutHashUrl = '';
11400 if (isUndefined(withoutBaseUrl)) {
11401 appBase = url;
11402 this.replace();
11403 }
11404 }
11405 }
11406
11407 parseAppUrl(withoutHashUrl, this);
932411408
932511409 this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase);
932611410
933711421 * Inside of Angular, we're always using pathnames that
933811422 * do not include drive names for routing.
933911423 */
9340 function removeWindowsDriveName (path, url, base) {
11424 function removeWindowsDriveName(path, url, base) {
934111425 /*
934211426 Matches paths for file protocol on windows,
934311427 such as /C:/foo/bar, and captures only /foo/bar.
937311457 this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
937411458 };
937511459
9376 this.$$rewrite = function(url) {
9377 if(stripHash(appBase) == stripHash(url)) {
9378 return url;
9379 }
11460 this.$$parseLinkUrl = function(url, relHref) {
11461 if (stripHash(appBase) == stripHash(url)) {
11462 this.$$parse(url);
11463 return true;
11464 }
11465 return false;
938011466 };
938111467 }
938211468
939611482
939711483 var appBaseNoFile = stripFile(appBase);
939811484
9399 this.$$rewrite = function(url) {
11485 this.$$parseLinkUrl = function(url, relHref) {
11486 if (relHref && relHref[0] === '#') {
11487 // special case for links to hash fragments:
11488 // keep the old url and only replace the hash fragment
11489 this.hash(relHref.slice(1));
11490 return true;
11491 }
11492
11493 var rewrittenUrl;
940011494 var appUrl;
940111495
9402 if ( appBase == stripHash(url) ) {
9403 return url;
9404 } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) {
9405 return appBase + hashPrefix + appUrl;
9406 } else if ( appBaseNoFile === url + '/') {
9407 return appBaseNoFile;
9408 }
11496 if (appBase == stripHash(url)) {
11497 rewrittenUrl = url;
11498 } else if ((appUrl = beginsWith(appBaseNoFile, url))) {
11499 rewrittenUrl = appBase + hashPrefix + appUrl;
11500 } else if (appBaseNoFile === url + '/') {
11501 rewrittenUrl = appBaseNoFile;
11502 }
11503 if (rewrittenUrl) {
11504 this.$$parse(rewrittenUrl);
11505 }
11506 return !!rewrittenUrl;
940911507 };
941011508
941111509 this.$$compose = function() {
942011518 }
942111519
942211520
9423 LocationHashbangInHtml5Url.prototype =
9424 LocationHashbangUrl.prototype =
9425 LocationHtml5Url.prototype = {
11521 var locationPrototype = {
942611522
942711523 /**
942811524 * Are we in html5 mode?
943111527 $$html5: false,
943211528
943311529 /**
9434 * Has any change been replacing ?
11530 * Has any change been replacing?
943511531 * @private
943611532 */
943711533 $$replace: false,
944611542 * Return full url representation with all segments encoded according to rules specified in
944711543 * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
944811544 *
11545 *
11546 * ```js
11547 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
11548 * var absUrl = $location.absUrl();
11549 * // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
11550 * ```
11551 *
944911552 * @return {string} full url
945011553 */
945111554 absUrl: locationGetter('$$absUrl'),
946111564 *
946211565 * Change path, search and hash, when called with parameter and return `$location`.
946311566 *
11567 *
11568 * ```js
11569 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
11570 * var url = $location.url();
11571 * // => "/some/path?foo=bar&baz=xoxo"
11572 * ```
11573 *
946411574 * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
9465 * @param {string=} replace The path that will be changed
946611575 * @return {string} url
946711576 */
9468 url: function(url, replace) {
9469 if (isUndefined(url))
11577 url: function(url) {
11578 if (isUndefined(url)) {
947011579 return this.$$url;
11580 }
947111581
947211582 var match = PATH_MATCH.exec(url);
9473 if (match[1]) this.path(decodeURIComponent(match[1]));
9474 if (match[2] || match[1]) this.search(match[3] || '');
9475 this.hash(match[5] || '', replace);
11583 if (match[1] || url === '') this.path(decodeURIComponent(match[1]));
11584 if (match[2] || match[1] || url === '') this.search(match[3] || '');
11585 this.hash(match[5] || '');
947611586
947711587 return this;
947811588 },
948611596 *
948711597 * Return protocol of current url.
948811598 *
11599 *
11600 * ```js
11601 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
11602 * var protocol = $location.protocol();
11603 * // => "http"
11604 * ```
11605 *
948911606 * @return {string} protocol of current url
949011607 */
949111608 protocol: locationGetter('$$protocol'),
949911616 *
950011617 * Return host of current url.
950111618 *
11619 * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
11620 *
11621 *
11622 * ```js
11623 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
11624 * var host = $location.host();
11625 * // => "example.com"
11626 *
11627 * // given url http://user:[email protected]:8080/#/some/path?foo=bar&baz=xoxo
11628 * host = $location.host();
11629 * // => "example.com"
11630 * host = location.host;
11631 * // => "example.com:8080"
11632 * ```
11633 *
950211634 * @return {string} host of current url.
950311635 */
950411636 host: locationGetter('$$host'),
951211644 *
951311645 * Return port of current url.
951411646 *
11647 *
11648 * ```js
11649 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
11650 * var port = $location.port();
11651 * // => 80
11652 * ```
11653 *
951511654 * @return {Number} port
951611655 */
951711656 port: locationGetter('$$port'),
953011669 * Note: Path should always begin with forward slash (/), this method will add the forward slash
953111670 * if it is missing.
953211671 *
9533 * @param {string=} path New path
11672 *
11673 * ```js
11674 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
11675 * var path = $location.path();
11676 * // => "/some/path"
11677 * ```
11678 *
11679 * @param {(string|number)=} path New path
953411680 * @return {string} path
953511681 */
953611682 path: locationGetterSetter('$$path', function(path) {
11683 path = path !== null ? path.toString() : '';
953711684 return path.charAt(0) == '/' ? path : '/' + path;
953811685 }),
953911686
955411701 * var searchObject = $location.search();
955511702 * // => {foo: 'bar', baz: 'xoxo'}
955611703 *
9557 *
955811704 * // set foo to 'yipee'
955911705 * $location.search('foo', 'yipee');
9560 * // => $location
11706 * // $location.search() => {foo: 'yipee', baz: 'xoxo'}
956111707 * ```
956211708 *
956311709 * @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or
956911715 * If the argument is a hash object containing an array of values, these values will be encoded
957011716 * as duplicate search parameters in the url.
957111717 *
9572 * @param {(string|Array<string>|boolean)=} paramValue If `search` is a string, then `paramValue`
11718 * @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue`
957311719 * will override only a single search property.
957411720 *
957511721 * If `paramValue` is an array, it will override the property of the `search` component of
958811734 case 0:
958911735 return this.$$search;
959011736 case 1:
9591 if (isString(search)) {
11737 if (isString(search) || isNumber(search)) {
11738 search = search.toString();
959211739 this.$$search = parseKeyValue(search);
959311740 } else if (isObject(search)) {
11741 search = copy(search, {});
959411742 // remove object undefined or null properties
959511743 forEach(search, function(value, key) {
959611744 if (value == null) delete search[key];
962511773 *
962611774 * Change hash fragment when called with parameter and return `$location`.
962711775 *
9628 * @param {string=} hash New hash fragment
11776 *
11777 * ```js
11778 * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
11779 * var hash = $location.hash();
11780 * // => "hashValue"
11781 * ```
11782 *
11783 * @param {(string|number)=} hash New hash fragment
962911784 * @return {string} hash
963011785 */
9631 hash: locationGetterSetter('$$hash', identity),
11786 hash: locationGetterSetter('$$hash', function(hash) {
11787 return hash !== null ? hash.toString() : '';
11788 }),
963211789
963311790 /**
963411791 * @ngdoc method
964411801 }
964511802 };
964611803
11804 forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) {
11805 Location.prototype = Object.create(locationPrototype);
11806
11807 /**
11808 * @ngdoc method
11809 * @name $location#state
11810 *
11811 * @description
11812 * This method is getter / setter.
11813 *
11814 * Return the history state object when called without any parameter.
11815 *
11816 * Change the history state object when called with one parameter and return `$location`.
11817 * The state object is later passed to `pushState` or `replaceState`.
11818 *
11819 * NOTE: This method is supported only in HTML5 mode and only in browsers supporting
11820 * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
11821 * older browsers (like IE9 or Android < 4.0), don't use this method.
11822 *
11823 * @param {object=} state State object for pushState or replaceState
11824 * @return {object} state
11825 */
11826 Location.prototype.state = function(state) {
11827 if (!arguments.length) {
11828 return this.$$state;
11829 }
11830
11831 if (Location !== LocationHtml5Url || !this.$$html5) {
11832 throw $locationMinErr('nostate', 'History API state support is available only ' +
11833 'in HTML5 mode and only in browsers supporting HTML5 History API');
11834 }
11835 // The user might modify `stateObject` after invoking `$location.state(stateObject)`
11836 // but we're changing the $$state reference to $browser.state() during the $digest
11837 // so the modification window is narrow.
11838 this.$$state = isUndefined(state) ? null : state;
11839
11840 return this;
11841 };
11842 });
11843
11844
964711845 function locationGetter(property) {
964811846 return function() {
964911847 return this[property];
965311851
965411852 function locationGetterSetter(property, preprocess) {
965511853 return function(value) {
9656 if (isUndefined(value))
11854 if (isUndefined(value)) {
965711855 return this[property];
11856 }
965811857
965911858 this[property] = preprocess(value);
966011859 this.$$compose();
969611895 * @description
969711896 * Use the `$locationProvider` to configure how the application deep linking paths are stored.
969811897 */
9699 function $LocationProvider(){
11898 function $LocationProvider() {
970011899 var hashPrefix = '',
9701 html5Mode = false;
11900 html5Mode = {
11901 enabled: false,
11902 requireBase: true,
11903 rewriteLinks: true
11904 };
970211905
970311906 /**
970411907 * @ngdoc method
972011923 * @ngdoc method
972111924 * @name $locationProvider#html5Mode
972211925 * @description
9723 * @param {boolean=} mode Use HTML5 strategy if available.
9724 * @returns {*} current value if used as getter or itself (chaining) if used as setter
11926 * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value.
11927 * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported
11928 * properties:
11929 * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to
11930 * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not
11931 * support `pushState`.
11932 * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies
11933 * whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are
11934 * true, and a base tag is not present, an error will be thrown when `$location` is injected.
11935 * See the {@link guide/$location $location guide for more information}
11936 * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled,
11937 * enables/disables url rewriting for relative links.
11938 *
11939 * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
972511940 */
972611941 this.html5Mode = function(mode) {
9727 if (isDefined(mode)) {
9728 html5Mode = mode;
11942 if (isBoolean(mode)) {
11943 html5Mode.enabled = mode;
11944 return this;
11945 } else if (isObject(mode)) {
11946
11947 if (isBoolean(mode.enabled)) {
11948 html5Mode.enabled = mode.enabled;
11949 }
11950
11951 if (isBoolean(mode.requireBase)) {
11952 html5Mode.requireBase = mode.requireBase;
11953 }
11954
11955 if (isBoolean(mode.rewriteLinks)) {
11956 html5Mode.rewriteLinks = mode.rewriteLinks;
11957 }
11958
972911959 return this;
973011960 } else {
973111961 return html5Mode;
973711967 * @name $location#$locationChangeStart
973811968 * @eventType broadcast on root scope
973911969 * @description
9740 * Broadcasted before a URL will change. This change can be prevented by calling
11970 * Broadcasted before a URL will change.
11971 *
11972 * This change can be prevented by calling
974111973 * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more
974211974 * details about event object. Upon successful change
9743 * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired.
11975 * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired.
11976 *
11977 * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
11978 * the browser supports the HTML5 History API.
974411979 *
974511980 * @param {Object} angularEvent Synthetic event object.
974611981 * @param {string} newUrl New URL
974711982 * @param {string=} oldUrl URL that was before it was changed.
11983 * @param {string=} newState New history state object
11984 * @param {string=} oldState History state object that was before it was changed.
974811985 */
974911986
975011987 /**
975411991 * @description
975511992 * Broadcasted after a URL was changed.
975611993 *
11994 * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
11995 * the browser supports the HTML5 History API.
11996 *
975711997 * @param {Object} angularEvent Synthetic event object.
975811998 * @param {string} newUrl New URL
975911999 * @param {string=} oldUrl URL that was before it was changed.
12000 * @param {string=} newState New history state object
12001 * @param {string=} oldState History state object that was before it was changed.
976012002 */
976112003
9762 this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
9763 function( $rootScope, $browser, $sniffer, $rootElement) {
12004 this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window',
12005 function($rootScope, $browser, $sniffer, $rootElement, $window) {
976412006 var $location,
976512007 LocationMode,
976612008 baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to ''
976712009 initialUrl = $browser.url(),
976812010 appBase;
976912011
9770 if (html5Mode) {
12012 if (html5Mode.enabled) {
12013 if (!baseHref && html5Mode.requireBase) {
12014 throw $locationMinErr('nobase',
12015 "$location in HTML5 mode requires a <base> tag to be present!");
12016 }
977112017 appBase = serverBase(initialUrl) + (baseHref || '/');
977212018 LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
977312019 } else {
977512021 LocationMode = LocationHashbangUrl;
977612022 }
977712023 $location = new LocationMode(appBase, '#' + hashPrefix);
9778 $location.$$parse($location.$$rewrite(initialUrl));
12024 $location.$$parseLinkUrl(initialUrl, initialUrl);
12025
12026 $location.$$state = $browser.state();
977912027
978012028 var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
12029
12030 function setBrowserUrlWithFallback(url, replace, state) {
12031 var oldUrl = $location.url();
12032 var oldState = $location.$$state;
12033 try {
12034 $browser.url(url, replace, state);
12035
12036 // Make sure $location.state() returns referentially identical (not just deeply equal)
12037 // state object; this makes possible quick checking if the state changed in the digest
12038 // loop. Checking deep equality would be too expensive.
12039 $location.$$state = $browser.state();
12040 } catch (e) {
12041 // Restore old values if pushState fails
12042 $location.url(oldUrl);
12043 $location.$$state = oldState;
12044
12045 throw e;
12046 }
12047 }
978112048
978212049 $rootElement.on('click', function(event) {
978312050 // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
978412051 // currently we open nice url link and redirect then
978512052
9786 if (event.ctrlKey || event.metaKey || event.which == 2) return;
12053 if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return;
978712054
978812055 var elm = jqLite(event.target);
978912056
979012057 // traverse the DOM up to find first A tag
9791 while (lowercase(elm[0].nodeName) !== 'a') {
12058 while (nodeName_(elm[0]) !== 'a') {
979212059 // ignore rewriting if no A tag (reached root element, or no parent - removed from document)
979312060 if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
979412061 }
979512062
979612063 var absHref = elm.prop('href');
12064 // get the actual href attribute - see
12065 // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
12066 var relHref = elm.attr('href') || elm.attr('xlink:href');
979712067
979812068 if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') {
979912069 // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during
980412074 // Ignore when url is started with javascript: or mailto:
980512075 if (IGNORE_URI_REGEXP.test(absHref)) return;
980612076
9807 // Make relative links work in HTML5 mode for legacy browsers (or at least IE8 & 9)
9808 // The href should be a regular url e.g. /link/somewhere or link/somewhere or ../somewhere or
9809 // somewhere#anchor or http://example.com/somewhere
9810 if (LocationMode === LocationHashbangInHtml5Url) {
9811 // get the actual href attribute - see
9812 // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
9813 var href = elm.attr('href') || elm.attr('xlink:href');
9814
9815 if (href && href.indexOf('://') < 0) { // Ignore absolute URLs
9816 var prefix = '#' + hashPrefix;
9817 if (href[0] == '/') {
9818 // absolute path - replace old path
9819 absHref = appBase + prefix + href;
9820 } else if (href[0] == '#') {
9821 // local anchor
9822 absHref = appBase + prefix + ($location.path() || '/') + href;
9823 } else {
9824 // relative path - join with current path
9825 var stack = $location.path().split("/"),
9826 parts = href.split("/");
9827 if (stack.length === 2 && !stack[1]) stack.length = 1;
9828 for (var i=0; i<parts.length; i++) {
9829 if (parts[i] == ".")
9830 continue;
9831 else if (parts[i] == "..")
9832 stack.pop();
9833 else if (parts[i].length)
9834 stack.push(parts[i]);
9835 }
9836 absHref = appBase + prefix + stack.join('/');
12077 if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) {
12078 if ($location.$$parseLinkUrl(absHref, relHref)) {
12079 // We do a preventDefault for all urls that are part of the angular application,
12080 // in html5mode and also without, so that we are able to abort navigation without
12081 // getting double entries in the location history.
12082 event.preventDefault();
12083 // update location manually
12084 if ($location.absUrl() != $browser.url()) {
12085 $rootScope.$apply();
12086 // hack to work around FF6 bug 684208 when scenario runner clicks on links
12087 $window.angular['ff-684208-preventDefault'] = true;
983712088 }
983812089 }
983912090 }
9840
9841 var rewrittenUrl = $location.$$rewrite(absHref);
9842
9843 if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) {
9844 event.preventDefault();
9845 if (rewrittenUrl != $browser.url()) {
9846 // update location manually
9847 $location.$$parse(rewrittenUrl);
9848 $rootScope.$apply();
9849 // hack to work around FF6 bug 684208 when scenario runner clicks on links
9850 window.angular['ff-684208-preventDefault'] = true;
12091 });
12092
12093
12094 // rewrite hashbang url <> html5 url
12095 if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) {
12096 $browser.url($location.absUrl(), true);
12097 }
12098
12099 var initializing = true;
12100
12101 // update $location when $browser url changes
12102 $browser.onUrlChange(function(newUrl, newState) {
12103 $rootScope.$evalAsync(function() {
12104 var oldUrl = $location.absUrl();
12105 var oldState = $location.$$state;
12106 var defaultPrevented;
12107
12108 $location.$$parse(newUrl);
12109 $location.$$state = newState;
12110
12111 defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
12112 newState, oldState).defaultPrevented;
12113
12114 // if the location was changed by a `$locationChangeStart` handler then stop
12115 // processing this location change
12116 if ($location.absUrl() !== newUrl) return;
12117
12118 if (defaultPrevented) {
12119 $location.$$parse(oldUrl);
12120 $location.$$state = oldState;
12121 setBrowserUrlWithFallback(oldUrl, false, oldState);
12122 } else {
12123 initializing = false;
12124 afterLocationChange(oldUrl, oldState);
985112125 }
9852 }
12126 });
12127 if (!$rootScope.$$phase) $rootScope.$digest();
985312128 });
985412129
9855
9856 // rewrite hashbang url <> html5 url
9857 if ($location.absUrl() != initialUrl) {
9858 $browser.url($location.absUrl(), true);
9859 }
9860
9861 // update $location when $browser url changes
9862 $browser.onUrlChange(function(newUrl) {
9863 if ($location.absUrl() != newUrl) {
12130 // update browser
12131 $rootScope.$watch(function $locationWatch() {
12132 var oldUrl = trimEmptyHash($browser.url());
12133 var newUrl = trimEmptyHash($location.absUrl());
12134 var oldState = $browser.state();
12135 var currentReplace = $location.$$replace;
12136 var urlOrStateChanged = oldUrl !== newUrl ||
12137 ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
12138
12139 if (initializing || urlOrStateChanged) {
12140 initializing = false;
12141
986412142 $rootScope.$evalAsync(function() {
9865 var oldUrl = $location.absUrl();
9866
9867 $location.$$parse(newUrl);
9868 if ($rootScope.$broadcast('$locationChangeStart', newUrl,
9869 oldUrl).defaultPrevented) {
12143 var newUrl = $location.absUrl();
12144 var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
12145 $location.$$state, oldState).defaultPrevented;
12146
12147 // if the location was changed by a `$locationChangeStart` handler then stop
12148 // processing this location change
12149 if ($location.absUrl() !== newUrl) return;
12150
12151 if (defaultPrevented) {
987012152 $location.$$parse(oldUrl);
9871 $browser.url(oldUrl);
12153 $location.$$state = oldState;
987212154 } else {
9873 afterLocationChange(oldUrl);
12155 if (urlOrStateChanged) {
12156 setBrowserUrlWithFallback(newUrl, currentReplace,
12157 oldState === $location.$$state ? null : $location.$$state);
12158 }
12159 afterLocationChange(oldUrl, oldState);
987412160 }
987512161 });
9876 if (!$rootScope.$$phase) $rootScope.$digest();
9877 }
12162 }
12163
12164 $location.$$replace = false;
12165
12166 // we don't need to return anything because $evalAsync will make the digest loop dirty when
12167 // there is a change
987812168 });
987912169
9880 // update browser
9881 var changeCounter = 0;
9882 $rootScope.$watch(function $locationWatch() {
9883 var oldUrl = $browser.url();
9884 var currentReplace = $location.$$replace;
9885
9886 if (!changeCounter || oldUrl != $location.absUrl()) {
9887 changeCounter++;
9888 $rootScope.$evalAsync(function() {
9889 if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
9890 defaultPrevented) {
9891 $location.$$parse(oldUrl);
9892 } else {
9893 $browser.url($location.absUrl(), currentReplace);
9894 afterLocationChange(oldUrl);
9895 }
9896 });
9897 }
9898 $location.$$replace = false;
9899
9900 return changeCounter;
9901 });
9902
990312170 return $location;
990412171
9905 function afterLocationChange(oldUrl) {
9906 $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
12172 function afterLocationChange(oldUrl, oldState) {
12173 $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl,
12174 $location.$$state, oldState);
990712175 }
990812176 }];
990912177 }
993412202 <file name="index.html">
993512203 <div ng-controller="LogController">
993612204 <p>Reload this page with open console, enter text and hit the log button...</p>
9937 Message:
9938 <input type="text" ng-model="message"/>
12205 <label>Message:
12206 <input type="text" ng-model="message" /></label>
993912207 <button ng-click="$log.log(message)">log</button>
994012208 <button ng-click="$log.warn(message)">warn</button>
994112209 <button ng-click="$log.info(message)">info</button>
994212210 <button ng-click="$log.error(message)">error</button>
12211 <button ng-click="$log.debug(message)">debug</button>
994312212 </div>
994412213 </file>
994512214 </example>
995112220 * @description
995212221 * Use the `$logProvider` to configure how the application logs messages
995312222 */
9954 function $LogProvider(){
12223 function $LogProvider() {
995512224 var debug = true,
995612225 self = this;
995712226
997112240 }
997212241 };
997312242
9974 this.$get = ['$window', function($window){
12243 this.$get = ['$window', function($window) {
997512244 return {
997612245 /**
997712246 * @ngdoc method
1001612285 * @description
1001712286 * Write a debug message
1001812287 */
10019 debug: (function () {
12288 debug: (function() {
1002012289 var fn = consoleLog('debug');
1002112290
1002212291 return function() {
1007012339 }];
1007112340 }
1007212341
12342 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
12343 * Any commits to this file should be reviewed with security in mind. *
12344 * Changes to this file can potentially create security vulnerabilities. *
12345 * An approval from 2 Core members with history of modifying *
12346 * this file is required. *
12347 * *
12348 * Does the change somehow allow for arbitrary javascript to be executed? *
12349 * Or allows for someone to change the prototype of built-in objects? *
12350 * Or gives undesired access to variables likes document or window? *
12351 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
12352
1007312353 var $parseMinErr = minErr('$parse');
10074 var promiseWarningCache = {};
10075 var promiseWarning;
1007612354
1007712355 // Sandboxing Angular Expressions
1007812356 // ------------------------------
1007912357 // Angular expressions are generally considered safe because these expressions only have direct
10080 // access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by
12358 // access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by
1008112359 // obtaining a reference to native JS functions such as the Function constructor.
1008212360 //
1008312361 // As an example, consider the following Angular expression:
1008612364 //
1008712365 // This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
1008812366 // against the expression language, but not to prevent exploits that were enabled by exposing
10089 // sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good
12367 // sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good
1009012368 // practice and therefore we are not even trying to protect against interaction with an object
1009112369 // explicitly exposed in this way.
1009212370 //
1009412372 // window or some DOM object that has a reference to window is published onto a Scope.
1009512373 // Similarly we prevent invocations of function known to be dangerous, as well as assignments to
1009612374 // native objects.
12375 //
12376 // See https://docs.angularjs.org/guide/security
1009712377
1009812378
1009912379 function ensureSafeMemberName(name, fullExpression) {
1010212382 || name === "__proto__") {
1010312383 throw $parseMinErr('isecfld',
1010412384 'Attempting to access a disallowed field in Angular expressions! '
10105 +'Expression: {0}', fullExpression);
12385 + 'Expression: {0}', fullExpression);
1010612386 }
1010712387 return name;
1010812388 }
1011512395 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
1011612396 fullExpression);
1011712397 } else if (// isWindow(obj)
10118 obj.document && obj.location && obj.alert && obj.setInterval) {
12398 obj.window === obj) {
1011912399 throw $parseMinErr('isecwindow',
1012012400 'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
1012112401 fullExpression);
1014412424 throw $parseMinErr('isecfn',
1014512425 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
1014612426 fullExpression);
10147 } else if (obj === CALL || obj === APPLY || (BIND && obj === BIND)) {
12427 } else if (obj === CALL || obj === APPLY || obj === BIND) {
1014812428 throw $parseMinErr('isecff',
1014912429 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}',
1015012430 fullExpression);
1015212432 }
1015312433 }
1015412434
10155 var OPERATORS = {
10156 /* jshint bitwise : false */
10157 'null':function(){return null;},
10158 'true':function(){return true;},
10159 'false':function(){return false;},
10160 undefined:noop,
10161 '+':function(self, locals, a,b){
10162 a=a(self, locals); b=b(self, locals);
10163 if (isDefined(a)) {
10164 if (isDefined(b)) {
10165 return a + b;
10166 }
10167 return a;
10168 }
10169 return isDefined(b)?b:undefined;},
10170 '-':function(self, locals, a,b){
10171 a=a(self, locals); b=b(self, locals);
10172 return (isDefined(a)?a:0)-(isDefined(b)?b:0);
10173 },
10174 '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);},
10175 '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);},
10176 '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);},
10177 '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);},
10178 '=':noop,
10179 '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);},
10180 '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);},
10181 '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);},
10182 '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);},
10183 '<':function(self, locals, a,b){return a(self, locals)<b(self, locals);},
10184 '>':function(self, locals, a,b){return a(self, locals)>b(self, locals);},
10185 '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);},
10186 '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);},
10187 '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);},
10188 '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);},
10189 '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);},
10190 // '|':function(self, locals, a,b){return a|b;},
10191 '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));},
10192 '!':function(self, locals, a){return !a(self, locals);}
10193 };
10194 /* jshint bitwise: true */
12435 var OPERATORS = createMap();
12436 forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
1019512437 var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
1019612438
1019712439
1020112443 /**
1020212444 * @constructor
1020312445 */
10204 var Lexer = function (options) {
12446 var Lexer = function(options) {
1020512447 this.options = options;
1020612448 };
1020712449
1020812450 Lexer.prototype = {
1020912451 constructor: Lexer,
1021012452
10211 lex: function (text) {
12453 lex: function(text) {
1021212454 this.text = text;
10213
1021412455 this.index = 0;
10215 this.ch = undefined;
10216 this.lastCh = ':'; // can start regexp
10217
1021812456 this.tokens = [];
1021912457
1022012458 while (this.index < this.text.length) {
10221 this.ch = this.text.charAt(this.index);
10222 if (this.is('"\'')) {
10223 this.readString(this.ch);
10224 } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) {
12459 var ch = this.text.charAt(this.index);
12460 if (ch === '"' || ch === "'") {
12461 this.readString(ch);
12462 } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
1022512463 this.readNumber();
10226 } else if (this.isIdent(this.ch)) {
12464 } else if (this.isIdent(ch)) {
1022712465 this.readIdent();
10228 } else if (this.is('(){}[].,;:?')) {
10229 this.tokens.push({
10230 index: this.index,
10231 text: this.ch
10232 });
12466 } else if (this.is(ch, '(){}[].,;:?')) {
12467 this.tokens.push({index: this.index, text: ch});
1023312468 this.index++;
10234 } else if (this.isWhitespace(this.ch)) {
12469 } else if (this.isWhitespace(ch)) {
1023512470 this.index++;
10236 continue;
1023712471 } else {
10238 var ch2 = this.ch + this.peek();
12472 var ch2 = ch + this.peek();
1023912473 var ch3 = ch2 + this.peek(2);
10240 var fn = OPERATORS[this.ch];
10241 var fn2 = OPERATORS[ch2];
10242 var fn3 = OPERATORS[ch3];
10243 if (fn3) {
10244 this.tokens.push({index: this.index, text: ch3, fn: fn3});
10245 this.index += 3;
10246 } else if (fn2) {
10247 this.tokens.push({index: this.index, text: ch2, fn: fn2});
10248 this.index += 2;
10249 } else if (fn) {
10250 this.tokens.push({
10251 index: this.index,
10252 text: this.ch,
10253 fn: fn
10254 });
10255 this.index += 1;
12474 var op1 = OPERATORS[ch];
12475 var op2 = OPERATORS[ch2];
12476 var op3 = OPERATORS[ch3];
12477 if (op1 || op2 || op3) {
12478 var token = op3 ? ch3 : (op2 ? ch2 : ch);
12479 this.tokens.push({index: this.index, text: token, operator: true});
12480 this.index += token.length;
1025612481 } else {
1025712482 this.throwError('Unexpected next character ', this.index, this.index + 1);
1025812483 }
1025912484 }
10260 this.lastCh = this.ch;
1026112485 }
1026212486 return this.tokens;
1026312487 },
1026412488
10265 is: function(chars) {
10266 return chars.indexOf(this.ch) !== -1;
10267 },
10268
10269 was: function(chars) {
10270 return chars.indexOf(this.lastCh) !== -1;
12489 is: function(ch, chars) {
12490 return chars.indexOf(ch) !== -1;
1027112491 },
1027212492
1027312493 peek: function(i) {
1027612496 },
1027712497
1027812498 isNumber: function(ch) {
10279 return ('0' <= ch && ch <= '9');
12499 return ('0' <= ch && ch <= '9') && typeof ch === "string";
1028012500 },
1028112501
1028212502 isWhitespace: function(ch) {
1032912549 }
1033012550 this.index++;
1033112551 }
10332 number = 1 * number;
1033312552 this.tokens.push({
1033412553 index: start,
1033512554 text: number,
10336 literal: true,
1033712555 constant: true,
10338 fn: function() { return number; }
12556 value: Number(number)
1033912557 });
1034012558 },
1034112559
1034212560 readIdent: function() {
10343 var parser = this;
10344
10345 var ident = '';
1034612561 var start = this.index;
10347
10348 var lastDot, peekIndex, methodName, ch;
10349
1035012562 while (this.index < this.text.length) {
10351 ch = this.text.charAt(this.index);
10352 if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) {
10353 if (ch === '.') lastDot = this.index;
10354 ident += ch;
10355 } else {
12563 var ch = this.text.charAt(this.index);
12564 if (!(this.isIdent(ch) || this.isNumber(ch))) {
1035612565 break;
1035712566 }
1035812567 this.index++;
1035912568 }
10360
10361 //check if this is not a method invocation and if it is back out to last dot
10362 if (lastDot) {
10363 peekIndex = this.index;
10364 while (peekIndex < this.text.length) {
10365 ch = this.text.charAt(peekIndex);
10366 if (ch === '(') {
10367 methodName = ident.substr(lastDot - start + 1);
10368 ident = ident.substr(0, lastDot - start);
10369 this.index = peekIndex;
10370 break;
10371 }
10372 if (this.isWhitespace(ch)) {
10373 peekIndex++;
10374 } else {
10375 break;
10376 }
10377 }
10378 }
10379
10380
10381 var token = {
12569 this.tokens.push({
1038212570 index: start,
10383 text: ident
10384 };
10385
10386 // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn
10387 if (OPERATORS.hasOwnProperty(ident)) {
10388 token.fn = OPERATORS[ident];
10389 token.literal = true;
10390 token.constant = true;
10391 } else {
10392 var getter = getterFn(ident, this.options, this.text);
10393 token.fn = extend(function(self, locals) {
10394 return (getter(self, locals));
10395 }, {
10396 assign: function(self, value) {
10397 return setter(self, ident, value, parser.text, parser.options);
10398 }
10399 });
10400 }
10401
10402 this.tokens.push(token);
10403
10404 if (methodName) {
10405 this.tokens.push({
10406 index:lastDot,
10407 text: '.'
10408 });
10409 this.tokens.push({
10410 index: lastDot + 1,
10411 text: methodName
10412 });
10413 }
12571 text: this.text.slice(start, this.index),
12572 identifier: true
12573 });
1041412574 },
1041512575
1041612576 readString: function(quote) {
1042512585 if (escape) {
1042612586 if (ch === 'u') {
1042712587 var hex = this.text.substring(this.index + 1, this.index + 5);
10428 if (!hex.match(/[\da-f]{4}/i))
12588 if (!hex.match(/[\da-f]{4}/i)) {
1042912589 this.throwError('Invalid unicode escape [\\u' + hex + ']');
12590 }
1043012591 this.index += 4;
1043112592 string += String.fromCharCode(parseInt(hex, 16));
1043212593 } else {
1044112602 this.tokens.push({
1044212603 index: start,
1044312604 text: rawString,
10444 string: string,
10445 literal: true,
1044612605 constant: true,
10447 fn: function() { return string; }
12606 value: string
1044812607 });
1044912608 return;
1045012609 } else {
1045612615 }
1045712616 };
1045812617
10459
10460 /**
10461 * @constructor
10462 */
10463 var Parser = function (lexer, $filter, options) {
12618 var AST = function(lexer, options) {
1046412619 this.lexer = lexer;
10465 this.$filter = $filter;
1046612620 this.options = options;
1046712621 };
1046812622
10469 Parser.ZERO = extend(function () {
10470 return 0;
10471 }, {
10472 constant: true
10473 });
10474
10475 Parser.prototype = {
10476 constructor: Parser,
10477
10478 parse: function (text) {
12623 AST.Program = 'Program';
12624 AST.ExpressionStatement = 'ExpressionStatement';
12625 AST.AssignmentExpression = 'AssignmentExpression';
12626 AST.ConditionalExpression = 'ConditionalExpression';
12627 AST.LogicalExpression = 'LogicalExpression';
12628 AST.BinaryExpression = 'BinaryExpression';
12629 AST.UnaryExpression = 'UnaryExpression';
12630 AST.CallExpression = 'CallExpression';
12631 AST.MemberExpression = 'MemberExpression';
12632 AST.Identifier = 'Identifier';
12633 AST.Literal = 'Literal';
12634 AST.ArrayExpression = 'ArrayExpression';
12635 AST.Property = 'Property';
12636 AST.ObjectExpression = 'ObjectExpression';
12637 AST.ThisExpression = 'ThisExpression';
12638
12639 // Internal use only
12640 AST.NGValueParameter = 'NGValueParameter';
12641
12642 AST.prototype = {
12643 ast: function(text) {
1047912644 this.text = text;
10480
1048112645 this.tokens = this.lexer.lex(text);
1048212646
10483 var value = this.statements();
12647 var value = this.program();
1048412648
1048512649 if (this.tokens.length !== 0) {
1048612650 this.throwError('is an unexpected token', this.tokens[0]);
1048712651 }
1048812652
10489 value.literal = !!value.literal;
10490 value.constant = !!value.constant;
10491
1049212653 return value;
1049312654 },
1049412655
10495 primary: function () {
12656 program: function() {
12657 var body = [];
12658 while (true) {
12659 if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
12660 body.push(this.expressionStatement());
12661 if (!this.expect(';')) {
12662 return { type: AST.Program, body: body};
12663 }
12664 }
12665 },
12666
12667 expressionStatement: function() {
12668 return { type: AST.ExpressionStatement, expression: this.filterChain() };
12669 },
12670
12671 filterChain: function() {
12672 var left = this.expression();
12673 var token;
12674 while ((token = this.expect('|'))) {
12675 left = this.filter(left);
12676 }
12677 return left;
12678 },
12679
12680 expression: function() {
12681 return this.assignment();
12682 },
12683
12684 assignment: function() {
12685 var result = this.ternary();
12686 if (this.expect('=')) {
12687 result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='};
12688 }
12689 return result;
12690 },
12691
12692 ternary: function() {
12693 var test = this.logicalOR();
12694 var alternate;
12695 var consequent;
12696 if (this.expect('?')) {
12697 alternate = this.expression();
12698 if (this.consume(':')) {
12699 consequent = this.expression();
12700 return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent};
12701 }
12702 }
12703 return test;
12704 },
12705
12706 logicalOR: function() {
12707 var left = this.logicalAND();
12708 while (this.expect('||')) {
12709 left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() };
12710 }
12711 return left;
12712 },
12713
12714 logicalAND: function() {
12715 var left = this.equality();
12716 while (this.expect('&&')) {
12717 left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()};
12718 }
12719 return left;
12720 },
12721
12722 equality: function() {
12723 var left = this.relational();
12724 var token;
12725 while ((token = this.expect('==','!=','===','!=='))) {
12726 left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() };
12727 }
12728 return left;
12729 },
12730
12731 relational: function() {
12732 var left = this.additive();
12733 var token;
12734 while ((token = this.expect('<', '>', '<=', '>='))) {
12735 left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() };
12736 }
12737 return left;
12738 },
12739
12740 additive: function() {
12741 var left = this.multiplicative();
12742 var token;
12743 while ((token = this.expect('+','-'))) {
12744 left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() };
12745 }
12746 return left;
12747 },
12748
12749 multiplicative: function() {
12750 var left = this.unary();
12751 var token;
12752 while ((token = this.expect('*','/','%'))) {
12753 left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() };
12754 }
12755 return left;
12756 },
12757
12758 unary: function() {
12759 var token;
12760 if ((token = this.expect('+', '-', '!'))) {
12761 return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() };
12762 } else {
12763 return this.primary();
12764 }
12765 },
12766
12767 primary: function() {
1049612768 var primary;
1049712769 if (this.expect('(')) {
1049812770 primary = this.filterChain();
1050112773 primary = this.arrayDeclaration();
1050212774 } else if (this.expect('{')) {
1050312775 primary = this.object();
12776 } else if (this.constants.hasOwnProperty(this.peek().text)) {
12777 primary = copy(this.constants[this.consume().text]);
12778 } else if (this.peek().identifier) {
12779 primary = this.identifier();
12780 } else if (this.peek().constant) {
12781 primary = this.constant();
1050412782 } else {
10505 var token = this.expect();
10506 primary = token.fn;
10507 if (!primary) {
10508 this.throwError('not a primary expression', token);
10509 }
10510 primary.literal = !!token.literal;
10511 primary.constant = !!token.constant;
10512 }
10513
10514 var next, context;
12783 this.throwError('not a primary expression', this.peek());
12784 }
12785
12786 var next;
1051512787 while ((next = this.expect('(', '[', '.'))) {
1051612788 if (next.text === '(') {
10517 primary = this.functionCall(primary, context);
10518 context = null;
12789 primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() };
12790 this.consume(')');
1051912791 } else if (next.text === '[') {
10520 context = primary;
10521 primary = this.objectIndex(primary);
12792 primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true };
12793 this.consume(']');
1052212794 } else if (next.text === '.') {
10523 context = primary;
10524 primary = this.fieldAccess(primary);
12795 primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false };
1052512796 } else {
1052612797 this.throwError('IMPOSSIBLE');
1052712798 }
1052912800 return primary;
1053012801 },
1053112802
10532 throwError: function(msg, token) {
10533 throw $parseMinErr('syntax',
10534 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].',
10535 token.text, msg, (token.index + 1), this.text, this.text.substring(token.index));
12803 filter: function(baseExpression) {
12804 var args = [baseExpression];
12805 var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true};
12806
12807 while (this.expect(':')) {
12808 args.push(this.expression());
12809 }
12810
12811 return result;
1053612812 },
1053712813
10538 peekToken: function() {
10539 if (this.tokens.length === 0)
10540 throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
10541 return this.tokens[0];
10542 },
10543
10544 peek: function(e1, e2, e3, e4) {
10545 if (this.tokens.length > 0) {
10546 var token = this.tokens[0];
10547 var t = token.text;
10548 if (t === e1 || t === e2 || t === e3 || t === e4 ||
10549 (!e1 && !e2 && !e3 && !e4)) {
10550 return token;
10551 }
10552 }
10553 return false;
10554 },
10555
10556 expect: function(e1, e2, e3, e4){
10557 var token = this.peek(e1, e2, e3, e4);
10558 if (token) {
10559 this.tokens.shift();
10560 return token;
10561 }
10562 return false;
10563 },
10564
10565 consume: function(e1){
10566 if (!this.expect(e1)) {
10567 this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
10568 }
10569 },
10570
10571 unaryFn: function(fn, right) {
10572 return extend(function(self, locals) {
10573 return fn(self, locals, right);
10574 }, {
10575 constant:right.constant
10576 });
10577 },
10578
10579 ternaryFn: function(left, middle, right){
10580 return extend(function(self, locals){
10581 return left(self, locals) ? middle(self, locals) : right(self, locals);
10582 }, {
10583 constant: left.constant && middle.constant && right.constant
10584 });
10585 },
10586
10587 binaryFn: function(left, fn, right) {
10588 return extend(function(self, locals) {
10589 return fn(self, locals, left, right);
10590 }, {
10591 constant:left.constant && right.constant
10592 });
10593 },
10594
10595 statements: function() {
10596 var statements = [];
10597 while (true) {
10598 if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
10599 statements.push(this.filterChain());
10600 if (!this.expect(';')) {
10601 // optimize for the common case where there is only one statement.
10602 // TODO(size): maybe we should not support multiple statements?
10603 return (statements.length === 1)
10604 ? statements[0]
10605 : function(self, locals) {
10606 var value;
10607 for (var i = 0; i < statements.length; i++) {
10608 var statement = statements[i];
10609 if (statement) {
10610 value = statement(self, locals);
10611 }
10612 }
10613 return value;
10614 };
10615 }
10616 }
10617 },
10618
10619 filterChain: function() {
10620 var left = this.expression();
10621 var token;
10622 while (true) {
10623 if ((token = this.expect('|'))) {
10624 left = this.binaryFn(left, token.fn, this.filter());
10625 } else {
10626 return left;
10627 }
10628 }
10629 },
10630
10631 filter: function() {
10632 var token = this.expect();
10633 var fn = this.$filter(token.text);
10634 var argsFn = [];
10635 while (true) {
10636 if ((token = this.expect(':'))) {
10637 argsFn.push(this.expression());
10638 } else {
10639 var fnInvoke = function(self, locals, input) {
10640 var args = [input];
10641 for (var i = 0; i < argsFn.length; i++) {
10642 args.push(argsFn[i](self, locals));
10643 }
10644 return fn.apply(self, args);
10645 };
10646 return function() {
10647 return fnInvoke;
10648 };
10649 }
10650 }
10651 },
10652
10653 expression: function() {
10654 return this.assignment();
10655 },
10656
10657 assignment: function() {
10658 var left = this.ternary();
10659 var right;
10660 var token;
10661 if ((token = this.expect('='))) {
10662 if (!left.assign) {
10663 this.throwError('implies assignment but [' +
10664 this.text.substring(0, token.index) + '] can not be assigned to', token);
10665 }
10666 right = this.ternary();
10667 return function(scope, locals) {
10668 return left.assign(scope, right(scope, locals), locals);
10669 };
10670 }
10671 return left;
10672 },
10673
10674 ternary: function() {
10675 var left = this.logicalOR();
10676 var middle;
10677 var token;
10678 if ((token = this.expect('?'))) {
10679 middle = this.assignment();
10680 if ((token = this.expect(':'))) {
10681 return this.ternaryFn(left, middle, this.assignment());
10682 } else {
10683 this.throwError('expected :', token);
10684 }
10685 } else {
10686 return left;
10687 }
10688 },
10689
10690 logicalOR: function() {
10691 var left = this.logicalAND();
10692 var token;
10693 while (true) {
10694 if ((token = this.expect('||'))) {
10695 left = this.binaryFn(left, token.fn, this.logicalAND());
10696 } else {
10697 return left;
10698 }
10699 }
10700 },
10701
10702 logicalAND: function() {
10703 var left = this.equality();
10704 var token;
10705 if ((token = this.expect('&&'))) {
10706 left = this.binaryFn(left, token.fn, this.logicalAND());
10707 }
10708 return left;
10709 },
10710
10711 equality: function() {
10712 var left = this.relational();
10713 var token;
10714 if ((token = this.expect('==','!=','===','!=='))) {
10715 left = this.binaryFn(left, token.fn, this.equality());
10716 }
10717 return left;
10718 },
10719
10720 relational: function() {
10721 var left = this.additive();
10722 var token;
10723 if ((token = this.expect('<', '>', '<=', '>='))) {
10724 left = this.binaryFn(left, token.fn, this.relational());
10725 }
10726 return left;
10727 },
10728
10729 additive: function() {
10730 var left = this.multiplicative();
10731 var token;
10732 while ((token = this.expect('+','-'))) {
10733 left = this.binaryFn(left, token.fn, this.multiplicative());
10734 }
10735 return left;
10736 },
10737
10738 multiplicative: function() {
10739 var left = this.unary();
10740 var token;
10741 while ((token = this.expect('*','/','%'))) {
10742 left = this.binaryFn(left, token.fn, this.unary());
10743 }
10744 return left;
10745 },
10746
10747 unary: function() {
10748 var token;
10749 if (this.expect('+')) {
10750 return this.primary();
10751 } else if ((token = this.expect('-'))) {
10752 return this.binaryFn(Parser.ZERO, token.fn, this.unary());
10753 } else if ((token = this.expect('!'))) {
10754 return this.unaryFn(token.fn, this.unary());
10755 } else {
10756 return this.primary();
10757 }
10758 },
10759
10760 fieldAccess: function(object) {
10761 var parser = this;
10762 var field = this.expect().text;
10763 var getter = getterFn(field, this.options, this.text);
10764
10765 return extend(function(scope, locals, self) {
10766 return getter(self || object(scope, locals));
10767 }, {
10768 assign: function(scope, value, locals) {
10769 var o = object(scope, locals);
10770 if (!o) object.assign(scope, o = {});
10771 return setter(o, field, value, parser.text, parser.options);
10772 }
10773 });
10774 },
10775
10776 objectIndex: function(obj) {
10777 var parser = this;
10778
10779 var indexFn = this.expression();
10780 this.consume(']');
10781
10782 return extend(function(self, locals) {
10783 var o = obj(self, locals),
10784 i = indexFn(self, locals),
10785 v, p;
10786
10787 ensureSafeMemberName(i, parser.text);
10788 if (!o) return undefined;
10789 v = ensureSafeObject(o[i], parser.text);
10790 if (v && v.then && parser.options.unwrapPromises) {
10791 p = v;
10792 if (!('$$v' in v)) {
10793 p.$$v = undefined;
10794 p.then(function(val) { p.$$v = val; });
10795 }
10796 v = v.$$v;
10797 }
10798 return v;
10799 }, {
10800 assign: function(self, value, locals) {
10801 var key = ensureSafeMemberName(indexFn(self, locals), parser.text);
10802 // prevent overwriting of Function.constructor which would break ensureSafeObject check
10803 var o = ensureSafeObject(obj(self, locals), parser.text);
10804 if (!o) obj.assign(self, o = {});
10805 return o[key] = value;
10806 }
10807 });
10808 },
10809
10810 functionCall: function(fn, contextGetter) {
10811 var argsFn = [];
12814 parseArguments: function() {
12815 var args = [];
1081212816 if (this.peekToken().text !== ')') {
1081312817 do {
10814 argsFn.push(this.expression());
12818 args.push(this.expression());
1081512819 } while (this.expect(','));
1081612820 }
10817 this.consume(')');
10818
10819 var parser = this;
10820
10821 return function(scope, locals) {
10822 var args = [];
10823 var context = contextGetter ? contextGetter(scope, locals) : scope;
10824
10825 for (var i = 0; i < argsFn.length; i++) {
10826 args.push(argsFn[i](scope, locals));
10827 }
10828 var fnPtr = fn(scope, locals, context) || noop;
10829
10830 ensureSafeObject(context, parser.text);
10831 ensureSafeFunction(fnPtr, parser.text);
10832
10833 // IE stupidity! (IE doesn't have apply for some native functions)
10834 var v = fnPtr.apply
10835 ? fnPtr.apply(context, args)
10836 : fnPtr(args[0], args[1], args[2], args[3], args[4]);
10837
10838 return ensureSafeObject(v, parser.text);
10839 };
12821 return args;
1084012822 },
1084112823
10842 // This is used with json array declaration
10843 arrayDeclaration: function () {
10844 var elementFns = [];
10845 var allConstant = true;
12824 identifier: function() {
12825 var token = this.consume();
12826 if (!token.identifier) {
12827 this.throwError('is not a valid identifier', token);
12828 }
12829 return { type: AST.Identifier, name: token.text };
12830 },
12831
12832 constant: function() {
12833 // TODO check that it is a constant
12834 return { type: AST.Literal, value: this.consume().value };
12835 },
12836
12837 arrayDeclaration: function() {
12838 var elements = [];
1084612839 if (this.peekToken().text !== ']') {
1084712840 do {
1084812841 if (this.peek(']')) {
1084912842 // Support trailing commas per ES5.1.
1085012843 break;
1085112844 }
10852 var elementFn = this.expression();
10853 elementFns.push(elementFn);
10854 if (!elementFn.constant) {
10855 allConstant = false;
10856 }
12845 elements.push(this.expression());
1085712846 } while (this.expect(','));
1085812847 }
1085912848 this.consume(']');
1086012849
10861 return extend(function(self, locals) {
10862 var array = [];
10863 for (var i = 0; i < elementFns.length; i++) {
10864 array.push(elementFns[i](self, locals));
10865 }
10866 return array;
10867 }, {
10868 literal: true,
10869 constant: allConstant
10870 });
12850 return { type: AST.ArrayExpression, elements: elements };
1087112851 },
1087212852
10873 object: function () {
10874 var keyValues = [];
10875 var allConstant = true;
12853 object: function() {
12854 var properties = [], property;
1087612855 if (this.peekToken().text !== '}') {
1087712856 do {
1087812857 if (this.peek('}')) {
1087912858 // Support trailing commas per ES5.1.
1088012859 break;
1088112860 }
10882 var token = this.expect(),
10883 key = token.string || token.text;
12861 property = {type: AST.Property, kind: 'init'};
12862 if (this.peek().constant) {
12863 property.key = this.constant();
12864 } else if (this.peek().identifier) {
12865 property.key = this.identifier();
12866 } else {
12867 this.throwError("invalid key", this.peek());
12868 }
1088412869 this.consume(':');
10885 var value = this.expression();
10886 keyValues.push({key: key, value: value});
10887 if (!value.constant) {
10888 allConstant = false;
10889 }
12870 property.value = this.expression();
12871 properties.push(property);
1089012872 } while (this.expect(','));
1089112873 }
1089212874 this.consume('}');
1089312875
10894 return extend(function(self, locals) {
10895 var object = {};
10896 for (var i = 0; i < keyValues.length; i++) {
10897 var keyValue = keyValues[i];
10898 object[keyValue.key] = keyValue.value(self, locals);
10899 }
10900 return object;
10901 }, {
10902 literal: true,
10903 constant: allConstant
10904 });
12876 return {type: AST.ObjectExpression, properties: properties };
12877 },
12878
12879 throwError: function(msg, token) {
12880 throw $parseMinErr('syntax',
12881 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].',
12882 token.text, msg, (token.index + 1), this.text, this.text.substring(token.index));
12883 },
12884
12885 consume: function(e1) {
12886 if (this.tokens.length === 0) {
12887 throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
12888 }
12889
12890 var token = this.expect(e1);
12891 if (!token) {
12892 this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
12893 }
12894 return token;
12895 },
12896
12897 peekToken: function() {
12898 if (this.tokens.length === 0) {
12899 throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
12900 }
12901 return this.tokens[0];
12902 },
12903
12904 peek: function(e1, e2, e3, e4) {
12905 return this.peekAhead(0, e1, e2, e3, e4);
12906 },
12907
12908 peekAhead: function(i, e1, e2, e3, e4) {
12909 if (this.tokens.length > i) {
12910 var token = this.tokens[i];
12911 var t = token.text;
12912 if (t === e1 || t === e2 || t === e3 || t === e4 ||
12913 (!e1 && !e2 && !e3 && !e4)) {
12914 return token;
12915 }
12916 }
12917 return false;
12918 },
12919
12920 expect: function(e1, e2, e3, e4) {
12921 var token = this.peek(e1, e2, e3, e4);
12922 if (token) {
12923 this.tokens.shift();
12924 return token;
12925 }
12926 return false;
12927 },
12928
12929
12930 /* `undefined` is not a constant, it is an identifier,
12931 * but using it as an identifier is not supported
12932 */
12933 constants: {
12934 'true': { type: AST.Literal, value: true },
12935 'false': { type: AST.Literal, value: false },
12936 'null': { type: AST.Literal, value: null },
12937 'undefined': {type: AST.Literal, value: undefined },
12938 'this': {type: AST.ThisExpression }
1090512939 }
1090612940 };
1090712941
12942 function ifDefined(v, d) {
12943 return typeof v !== 'undefined' ? v : d;
12944 }
12945
12946 function plusFn(l, r) {
12947 if (typeof l === 'undefined') return r;
12948 if (typeof r === 'undefined') return l;
12949 return l + r;
12950 }
12951
12952 function isStateless($filter, filterName) {
12953 var fn = $filter(filterName);
12954 return !fn.$stateful;
12955 }
12956
12957 function findConstantAndWatchExpressions(ast, $filter) {
12958 var allConstants;
12959 var argsToWatch;
12960 switch (ast.type) {
12961 case AST.Program:
12962 allConstants = true;
12963 forEach(ast.body, function(expr) {
12964 findConstantAndWatchExpressions(expr.expression, $filter);
12965 allConstants = allConstants && expr.expression.constant;
12966 });
12967 ast.constant = allConstants;
12968 break;
12969 case AST.Literal:
12970 ast.constant = true;
12971 ast.toWatch = [];
12972 break;
12973 case AST.UnaryExpression:
12974 findConstantAndWatchExpressions(ast.argument, $filter);
12975 ast.constant = ast.argument.constant;
12976 ast.toWatch = ast.argument.toWatch;
12977 break;
12978 case AST.BinaryExpression:
12979 findConstantAndWatchExpressions(ast.left, $filter);
12980 findConstantAndWatchExpressions(ast.right, $filter);
12981 ast.constant = ast.left.constant && ast.right.constant;
12982 ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch);
12983 break;
12984 case AST.LogicalExpression:
12985 findConstantAndWatchExpressions(ast.left, $filter);
12986 findConstantAndWatchExpressions(ast.right, $filter);
12987 ast.constant = ast.left.constant && ast.right.constant;
12988 ast.toWatch = ast.constant ? [] : [ast];
12989 break;
12990 case AST.ConditionalExpression:
12991 findConstantAndWatchExpressions(ast.test, $filter);
12992 findConstantAndWatchExpressions(ast.alternate, $filter);
12993 findConstantAndWatchExpressions(ast.consequent, $filter);
12994 ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant;
12995 ast.toWatch = ast.constant ? [] : [ast];
12996 break;
12997 case AST.Identifier:
12998 ast.constant = false;
12999 ast.toWatch = [ast];
13000 break;
13001 case AST.MemberExpression:
13002 findConstantAndWatchExpressions(ast.object, $filter);
13003 if (ast.computed) {
13004 findConstantAndWatchExpressions(ast.property, $filter);
13005 }
13006 ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
13007 ast.toWatch = [ast];
13008 break;
13009 case AST.CallExpression:
13010 allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false;
13011 argsToWatch = [];
13012 forEach(ast.arguments, function(expr) {
13013 findConstantAndWatchExpressions(expr, $filter);
13014 allConstants = allConstants && expr.constant;
13015 if (!expr.constant) {
13016 argsToWatch.push.apply(argsToWatch, expr.toWatch);
13017 }
13018 });
13019 ast.constant = allConstants;
13020 ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast];
13021 break;
13022 case AST.AssignmentExpression:
13023 findConstantAndWatchExpressions(ast.left, $filter);
13024 findConstantAndWatchExpressions(ast.right, $filter);
13025 ast.constant = ast.left.constant && ast.right.constant;
13026 ast.toWatch = [ast];
13027 break;
13028 case AST.ArrayExpression:
13029 allConstants = true;
13030 argsToWatch = [];
13031 forEach(ast.elements, function(expr) {
13032 findConstantAndWatchExpressions(expr, $filter);
13033 allConstants = allConstants && expr.constant;
13034 if (!expr.constant) {
13035 argsToWatch.push.apply(argsToWatch, expr.toWatch);
13036 }
13037 });
13038 ast.constant = allConstants;
13039 ast.toWatch = argsToWatch;
13040 break;
13041 case AST.ObjectExpression:
13042 allConstants = true;
13043 argsToWatch = [];
13044 forEach(ast.properties, function(property) {
13045 findConstantAndWatchExpressions(property.value, $filter);
13046 allConstants = allConstants && property.value.constant;
13047 if (!property.value.constant) {
13048 argsToWatch.push.apply(argsToWatch, property.value.toWatch);
13049 }
13050 });
13051 ast.constant = allConstants;
13052 ast.toWatch = argsToWatch;
13053 break;
13054 case AST.ThisExpression:
13055 ast.constant = false;
13056 ast.toWatch = [];
13057 break;
13058 }
13059 }
13060
13061 function getInputs(body) {
13062 if (body.length != 1) return;
13063 var lastExpression = body[0].expression;
13064 var candidate = lastExpression.toWatch;
13065 if (candidate.length !== 1) return candidate;
13066 return candidate[0] !== lastExpression ? candidate : undefined;
13067 }
13068
13069 function isAssignable(ast) {
13070 return ast.type === AST.Identifier || ast.type === AST.MemberExpression;
13071 }
13072
13073 function assignableAST(ast) {
13074 if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) {
13075 return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='};
13076 }
13077 }
13078
13079 function isLiteral(ast) {
13080 return ast.body.length === 0 ||
13081 ast.body.length === 1 && (
13082 ast.body[0].expression.type === AST.Literal ||
13083 ast.body[0].expression.type === AST.ArrayExpression ||
13084 ast.body[0].expression.type === AST.ObjectExpression);
13085 }
13086
13087 function isConstant(ast) {
13088 return ast.constant;
13089 }
13090
13091 function ASTCompiler(astBuilder, $filter) {
13092 this.astBuilder = astBuilder;
13093 this.$filter = $filter;
13094 }
13095
13096 ASTCompiler.prototype = {
13097 compile: function(expression, expensiveChecks) {
13098 var self = this;
13099 var ast = this.astBuilder.ast(expression);
13100 this.state = {
13101 nextId: 0,
13102 filters: {},
13103 expensiveChecks: expensiveChecks,
13104 fn: {vars: [], body: [], own: {}},
13105 assign: {vars: [], body: [], own: {}},
13106 inputs: []
13107 };
13108 findConstantAndWatchExpressions(ast, self.$filter);
13109 var extra = '';
13110 var assignable;
13111 this.stage = 'assign';
13112 if ((assignable = assignableAST(ast))) {
13113 this.state.computing = 'assign';
13114 var result = this.nextId();
13115 this.recurse(assignable, result);
13116 extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l');
13117 }
13118 var toWatch = getInputs(ast.body);
13119 self.stage = 'inputs';
13120 forEach(toWatch, function(watch, key) {
13121 var fnKey = 'fn' + key;
13122 self.state[fnKey] = {vars: [], body: [], own: {}};
13123 self.state.computing = fnKey;
13124 var intoId = self.nextId();
13125 self.recurse(watch, intoId);
13126 self.return_(intoId);
13127 self.state.inputs.push(fnKey);
13128 watch.watchId = key;
13129 });
13130 this.state.computing = 'fn';
13131 this.stage = 'main';
13132 this.recurse(ast);
13133 var fnString =
13134 // The build and minification steps remove the string "use strict" from the code, but this is done using a regex.
13135 // This is a workaround for this until we do a better job at only removing the prefix only when we should.
13136 '"' + this.USE + ' ' + this.STRICT + '";\n' +
13137 this.filterPrefix() +
13138 'var fn=' + this.generateFunction('fn', 's,l,a,i') +
13139 extra +
13140 this.watchFns() +
13141 'return fn;';
13142
13143 /* jshint -W054 */
13144 var fn = (new Function('$filter',
13145 'ensureSafeMemberName',
13146 'ensureSafeObject',
13147 'ensureSafeFunction',
13148 'ifDefined',
13149 'plus',
13150 'text',
13151 fnString))(
13152 this.$filter,
13153 ensureSafeMemberName,
13154 ensureSafeObject,
13155 ensureSafeFunction,
13156 ifDefined,
13157 plusFn,
13158 expression);
13159 /* jshint +W054 */
13160 this.state = this.stage = undefined;
13161 fn.literal = isLiteral(ast);
13162 fn.constant = isConstant(ast);
13163 return fn;
13164 },
13165
13166 USE: 'use',
13167
13168 STRICT: 'strict',
13169
13170 watchFns: function() {
13171 var result = [];
13172 var fns = this.state.inputs;
13173 var self = this;
13174 forEach(fns, function(name) {
13175 result.push('var ' + name + '=' + self.generateFunction(name, 's'));
13176 });
13177 if (fns.length) {
13178 result.push('fn.inputs=[' + fns.join(',') + '];');
13179 }
13180 return result.join('');
13181 },
13182
13183 generateFunction: function(name, params) {
13184 return 'function(' + params + '){' +
13185 this.varsPrefix(name) +
13186 this.body(name) +
13187 '};';
13188 },
13189
13190 filterPrefix: function() {
13191 var parts = [];
13192 var self = this;
13193 forEach(this.state.filters, function(id, filter) {
13194 parts.push(id + '=$filter(' + self.escape(filter) + ')');
13195 });
13196 if (parts.length) return 'var ' + parts.join(',') + ';';
13197 return '';
13198 },
13199
13200 varsPrefix: function(section) {
13201 return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : '';
13202 },
13203
13204 body: function(section) {
13205 return this.state[section].body.join('');
13206 },
13207
13208 recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
13209 var left, right, self = this, args, expression;
13210 recursionFn = recursionFn || noop;
13211 if (!skipWatchIdCheck && isDefined(ast.watchId)) {
13212 intoId = intoId || this.nextId();
13213 this.if_('i',
13214 this.lazyAssign(intoId, this.computedMember('i', ast.watchId)),
13215 this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true)
13216 );
13217 return;
13218 }
13219 switch (ast.type) {
13220 case AST.Program:
13221 forEach(ast.body, function(expression, pos) {
13222 self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; });
13223 if (pos !== ast.body.length - 1) {
13224 self.current().body.push(right, ';');
13225 } else {
13226 self.return_(right);
13227 }
13228 });
13229 break;
13230 case AST.Literal:
13231 expression = this.escape(ast.value);
13232 this.assign(intoId, expression);
13233 recursionFn(expression);
13234 break;
13235 case AST.UnaryExpression:
13236 this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; });
13237 expression = ast.operator + '(' + this.ifDefined(right, 0) + ')';
13238 this.assign(intoId, expression);
13239 recursionFn(expression);
13240 break;
13241 case AST.BinaryExpression:
13242 this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; });
13243 this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; });
13244 if (ast.operator === '+') {
13245 expression = this.plus(left, right);
13246 } else if (ast.operator === '-') {
13247 expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0);
13248 } else {
13249 expression = '(' + left + ')' + ast.operator + '(' + right + ')';
13250 }
13251 this.assign(intoId, expression);
13252 recursionFn(expression);
13253 break;
13254 case AST.LogicalExpression:
13255 intoId = intoId || this.nextId();
13256 self.recurse(ast.left, intoId);
13257 self.if_(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId));
13258 recursionFn(intoId);
13259 break;
13260 case AST.ConditionalExpression:
13261 intoId = intoId || this.nextId();
13262 self.recurse(ast.test, intoId);
13263 self.if_(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId));
13264 recursionFn(intoId);
13265 break;
13266 case AST.Identifier:
13267 intoId = intoId || this.nextId();
13268 if (nameId) {
13269 nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s');
13270 nameId.computed = false;
13271 nameId.name = ast.name;
13272 }
13273 ensureSafeMemberName(ast.name);
13274 self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)),
13275 function() {
13276 self.if_(self.stage === 'inputs' || 's', function() {
13277 if (create && create !== 1) {
13278 self.if_(
13279 self.not(self.nonComputedMember('s', ast.name)),
13280 self.lazyAssign(self.nonComputedMember('s', ast.name), '{}'));
13281 }
13282 self.assign(intoId, self.nonComputedMember('s', ast.name));
13283 });
13284 }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name))
13285 );
13286 if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) {
13287 self.addEnsureSafeObject(intoId);
13288 }
13289 recursionFn(intoId);
13290 break;
13291 case AST.MemberExpression:
13292 left = nameId && (nameId.context = this.nextId()) || this.nextId();
13293 intoId = intoId || this.nextId();
13294 self.recurse(ast.object, left, undefined, function() {
13295 self.if_(self.notNull(left), function() {
13296 if (ast.computed) {
13297 right = self.nextId();
13298 self.recurse(ast.property, right);
13299 self.addEnsureSafeMemberName(right);
13300 if (create && create !== 1) {
13301 self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
13302 }
13303 expression = self.ensureSafeObject(self.computedMember(left, right));
13304 self.assign(intoId, expression);
13305 if (nameId) {
13306 nameId.computed = true;
13307 nameId.name = right;
13308 }
13309 } else {
13310 ensureSafeMemberName(ast.property.name);
13311 if (create && create !== 1) {
13312 self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
13313 }
13314 expression = self.nonComputedMember(left, ast.property.name);
13315 if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) {
13316 expression = self.ensureSafeObject(expression);
13317 }
13318 self.assign(intoId, expression);
13319 if (nameId) {
13320 nameId.computed = false;
13321 nameId.name = ast.property.name;
13322 }
13323 }
13324 }, function() {
13325 self.assign(intoId, 'undefined');
13326 });
13327 recursionFn(intoId);
13328 }, !!create);
13329 break;
13330 case AST.CallExpression:
13331 intoId = intoId || this.nextId();
13332 if (ast.filter) {
13333 right = self.filter(ast.callee.name);
13334 args = [];
13335 forEach(ast.arguments, function(expr) {
13336 var argument = self.nextId();
13337 self.recurse(expr, argument);
13338 args.push(argument);
13339 });
13340 expression = right + '(' + args.join(',') + ')';
13341 self.assign(intoId, expression);
13342 recursionFn(intoId);
13343 } else {
13344 right = self.nextId();
13345 left = {};
13346 args = [];
13347 self.recurse(ast.callee, right, left, function() {
13348 self.if_(self.notNull(right), function() {
13349 self.addEnsureSafeFunction(right);
13350 forEach(ast.arguments, function(expr) {
13351 self.recurse(expr, self.nextId(), undefined, function(argument) {
13352 args.push(self.ensureSafeObject(argument));
13353 });
13354 });
13355 if (left.name) {
13356 if (!self.state.expensiveChecks) {
13357 self.addEnsureSafeObject(left.context);
13358 }
13359 expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')';
13360 } else {
13361 expression = right + '(' + args.join(',') + ')';
13362 }
13363 expression = self.ensureSafeObject(expression);
13364 self.assign(intoId, expression);
13365 }, function() {
13366 self.assign(intoId, 'undefined');
13367 });
13368 recursionFn(intoId);
13369 });
13370 }
13371 break;
13372 case AST.AssignmentExpression:
13373 right = this.nextId();
13374 left = {};
13375 if (!isAssignable(ast.left)) {
13376 throw $parseMinErr('lval', 'Trying to assing a value to a non l-value');
13377 }
13378 this.recurse(ast.left, undefined, left, function() {
13379 self.if_(self.notNull(left.context), function() {
13380 self.recurse(ast.right, right);
13381 self.addEnsureSafeObject(self.member(left.context, left.name, left.computed));
13382 expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
13383 self.assign(intoId, expression);
13384 recursionFn(intoId || expression);
13385 });
13386 }, 1);
13387 break;
13388 case AST.ArrayExpression:
13389 args = [];
13390 forEach(ast.elements, function(expr) {
13391 self.recurse(expr, self.nextId(), undefined, function(argument) {
13392 args.push(argument);
13393 });
13394 });
13395 expression = '[' + args.join(',') + ']';
13396 this.assign(intoId, expression);
13397 recursionFn(expression);
13398 break;
13399 case AST.ObjectExpression:
13400 args = [];
13401 forEach(ast.properties, function(property) {
13402 self.recurse(property.value, self.nextId(), undefined, function(expr) {
13403 args.push(self.escape(
13404 property.key.type === AST.Identifier ? property.key.name :
13405 ('' + property.key.value)) +
13406 ':' + expr);
13407 });
13408 });
13409 expression = '{' + args.join(',') + '}';
13410 this.assign(intoId, expression);
13411 recursionFn(expression);
13412 break;
13413 case AST.ThisExpression:
13414 this.assign(intoId, 's');
13415 recursionFn('s');
13416 break;
13417 case AST.NGValueParameter:
13418 this.assign(intoId, 'v');
13419 recursionFn('v');
13420 break;
13421 }
13422 },
13423
13424 getHasOwnProperty: function(element, property) {
13425 var key = element + '.' + property;
13426 var own = this.current().own;
13427 if (!own.hasOwnProperty(key)) {
13428 own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')');
13429 }
13430 return own[key];
13431 },
13432
13433 assign: function(id, value) {
13434 if (!id) return;
13435 this.current().body.push(id, '=', value, ';');
13436 return id;
13437 },
13438
13439 filter: function(filterName) {
13440 if (!this.state.filters.hasOwnProperty(filterName)) {
13441 this.state.filters[filterName] = this.nextId(true);
13442 }
13443 return this.state.filters[filterName];
13444 },
13445
13446 ifDefined: function(id, defaultValue) {
13447 return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')';
13448 },
13449
13450 plus: function(left, right) {
13451 return 'plus(' + left + ',' + right + ')';
13452 },
13453
13454 return_: function(id) {
13455 this.current().body.push('return ', id, ';');
13456 },
13457
13458 if_: function(test, alternate, consequent) {
13459 if (test === true) {
13460 alternate();
13461 } else {
13462 var body = this.current().body;
13463 body.push('if(', test, '){');
13464 alternate();
13465 body.push('}');
13466 if (consequent) {
13467 body.push('else{');
13468 consequent();
13469 body.push('}');
13470 }
13471 }
13472 },
13473
13474 not: function(expression) {
13475 return '!(' + expression + ')';
13476 },
13477
13478 notNull: function(expression) {
13479 return expression + '!=null';
13480 },
13481
13482 nonComputedMember: function(left, right) {
13483 return left + '.' + right;
13484 },
13485
13486 computedMember: function(left, right) {
13487 return left + '[' + right + ']';
13488 },
13489
13490 member: function(left, right, computed) {
13491 if (computed) return this.computedMember(left, right);
13492 return this.nonComputedMember(left, right);
13493 },
13494
13495 addEnsureSafeObject: function(item) {
13496 this.current().body.push(this.ensureSafeObject(item), ';');
13497 },
13498
13499 addEnsureSafeMemberName: function(item) {
13500 this.current().body.push(this.ensureSafeMemberName(item), ';');
13501 },
13502
13503 addEnsureSafeFunction: function(item) {
13504 this.current().body.push(this.ensureSafeFunction(item), ';');
13505 },
13506
13507 ensureSafeObject: function(item) {
13508 return 'ensureSafeObject(' + item + ',text)';
13509 },
13510
13511 ensureSafeMemberName: function(item) {
13512 return 'ensureSafeMemberName(' + item + ',text)';
13513 },
13514
13515 ensureSafeFunction: function(item) {
13516 return 'ensureSafeFunction(' + item + ',text)';
13517 },
13518
13519 lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
13520 var self = this;
13521 return function() {
13522 self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck);
13523 };
13524 },
13525
13526 lazyAssign: function(id, value) {
13527 var self = this;
13528 return function() {
13529 self.assign(id, value);
13530 };
13531 },
13532
13533 stringEscapeRegex: /[^ a-zA-Z0-9]/g,
13534
13535 stringEscapeFn: function(c) {
13536 return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
13537 },
13538
13539 escape: function(value) {
13540 if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'";
13541 if (isNumber(value)) return value.toString();
13542 if (value === true) return 'true';
13543 if (value === false) return 'false';
13544 if (value === null) return 'null';
13545 if (typeof value === 'undefined') return 'undefined';
13546
13547 throw $parseMinErr('esc', 'IMPOSSIBLE');
13548 },
13549
13550 nextId: function(skip, init) {
13551 var id = 'v' + (this.state.nextId++);
13552 if (!skip) {
13553 this.current().vars.push(id + (init ? '=' + init : ''));
13554 }
13555 return id;
13556 },
13557
13558 current: function() {
13559 return this.state[this.state.computing];
13560 }
13561 };
13562
13563
13564 function ASTInterpreter(astBuilder, $filter) {
13565 this.astBuilder = astBuilder;
13566 this.$filter = $filter;
13567 }
13568
13569 ASTInterpreter.prototype = {
13570 compile: function(expression, expensiveChecks) {
13571 var self = this;
13572 var ast = this.astBuilder.ast(expression);
13573 this.expression = expression;
13574 this.expensiveChecks = expensiveChecks;
13575 findConstantAndWatchExpressions(ast, self.$filter);
13576 var assignable;
13577 var assign;
13578 if ((assignable = assignableAST(ast))) {
13579 assign = this.recurse(assignable);
13580 }
13581 var toWatch = getInputs(ast.body);
13582 var inputs;
13583 if (toWatch) {
13584 inputs = [];
13585 forEach(toWatch, function(watch, key) {
13586 var input = self.recurse(watch);
13587 watch.input = input;
13588 inputs.push(input);
13589 watch.watchId = key;
13590 });
13591 }
13592 var expressions = [];
13593 forEach(ast.body, function(expression) {
13594 expressions.push(self.recurse(expression.expression));
13595 });
13596 var fn = ast.body.length === 0 ? function() {} :
13597 ast.body.length === 1 ? expressions[0] :
13598 function(scope, locals) {
13599 var lastValue;
13600 forEach(expressions, function(exp) {
13601 lastValue = exp(scope, locals);
13602 });
13603 return lastValue;
13604 };
13605 if (assign) {
13606 fn.assign = function(scope, value, locals) {
13607 return assign(scope, locals, value);
13608 };
13609 }
13610 if (inputs) {
13611 fn.inputs = inputs;
13612 }
13613 fn.literal = isLiteral(ast);
13614 fn.constant = isConstant(ast);
13615 return fn;
13616 },
13617
13618 recurse: function(ast, context, create) {
13619 var left, right, self = this, args, expression;
13620 if (ast.input) {
13621 return this.inputs(ast.input, ast.watchId);
13622 }
13623 switch (ast.type) {
13624 case AST.Literal:
13625 return this.value(ast.value, context);
13626 case AST.UnaryExpression:
13627 right = this.recurse(ast.argument);
13628 return this['unary' + ast.operator](right, context);
13629 case AST.BinaryExpression:
13630 left = this.recurse(ast.left);
13631 right = this.recurse(ast.right);
13632 return this['binary' + ast.operator](left, right, context);
13633 case AST.LogicalExpression:
13634 left = this.recurse(ast.left);
13635 right = this.recurse(ast.right);
13636 return this['binary' + ast.operator](left, right, context);
13637 case AST.ConditionalExpression:
13638 return this['ternary?:'](
13639 this.recurse(ast.test),
13640 this.recurse(ast.alternate),
13641 this.recurse(ast.consequent),
13642 context
13643 );
13644 case AST.Identifier:
13645 ensureSafeMemberName(ast.name, self.expression);
13646 return self.identifier(ast.name,
13647 self.expensiveChecks || isPossiblyDangerousMemberName(ast.name),
13648 context, create, self.expression);
13649 case AST.MemberExpression:
13650 left = this.recurse(ast.object, false, !!create);
13651 if (!ast.computed) {
13652 ensureSafeMemberName(ast.property.name, self.expression);
13653 right = ast.property.name;
13654 }
13655 if (ast.computed) right = this.recurse(ast.property);
13656 return ast.computed ?
13657 this.computedMember(left, right, context, create, self.expression) :
13658 this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression);
13659 case AST.CallExpression:
13660 args = [];
13661 forEach(ast.arguments, function(expr) {
13662 args.push(self.recurse(expr));
13663 });
13664 if (ast.filter) right = this.$filter(ast.callee.name);
13665 if (!ast.filter) right = this.recurse(ast.callee, true);
13666 return ast.filter ?
13667 function(scope, locals, assign, inputs) {
13668 var values = [];
13669 for (var i = 0; i < args.length; ++i) {
13670 values.push(args[i](scope, locals, assign, inputs));
13671 }
13672 var value = right.apply(undefined, values, inputs);
13673 return context ? {context: undefined, name: undefined, value: value} : value;
13674 } :
13675 function(scope, locals, assign, inputs) {
13676 var rhs = right(scope, locals, assign, inputs);
13677 var value;
13678 if (rhs.value != null) {
13679 ensureSafeObject(rhs.context, self.expression);
13680 ensureSafeFunction(rhs.value, self.expression);
13681 var values = [];
13682 for (var i = 0; i < args.length; ++i) {
13683 values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression));
13684 }
13685 value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression);
13686 }
13687 return context ? {value: value} : value;
13688 };
13689 case AST.AssignmentExpression:
13690 left = this.recurse(ast.left, true, 1);
13691 right = this.recurse(ast.right);
13692 return function(scope, locals, assign, inputs) {
13693 var lhs = left(scope, locals, assign, inputs);
13694 var rhs = right(scope, locals, assign, inputs);
13695 ensureSafeObject(lhs.value, self.expression);
13696 lhs.context[lhs.name] = rhs;
13697 return context ? {value: rhs} : rhs;
13698 };
13699 case AST.ArrayExpression:
13700 args = [];
13701 forEach(ast.elements, function(expr) {
13702 args.push(self.recurse(expr));
13703 });
13704 return function(scope, locals, assign, inputs) {
13705 var value = [];
13706 for (var i = 0; i < args.length; ++i) {
13707 value.push(args[i](scope, locals, assign, inputs));
13708 }
13709 return context ? {value: value} : value;
13710 };
13711 case AST.ObjectExpression:
13712 args = [];
13713 forEach(ast.properties, function(property) {
13714 args.push({key: property.key.type === AST.Identifier ?
13715 property.key.name :
13716 ('' + property.key.value),
13717 value: self.recurse(property.value)
13718 });
13719 });
13720 return function(scope, locals, assign, inputs) {
13721 var value = {};
13722 for (var i = 0; i < args.length; ++i) {
13723 value[args[i].key] = args[i].value(scope, locals, assign, inputs);
13724 }
13725 return context ? {value: value} : value;
13726 };
13727 case AST.ThisExpression:
13728 return function(scope) {
13729 return context ? {value: scope} : scope;
13730 };
13731 case AST.NGValueParameter:
13732 return function(scope, locals, assign, inputs) {
13733 return context ? {value: assign} : assign;
13734 };
13735 }
13736 },
13737
13738 'unary+': function(argument, context) {
13739 return function(scope, locals, assign, inputs) {
13740 var arg = argument(scope, locals, assign, inputs);
13741 if (isDefined(arg)) {
13742 arg = +arg;
13743 } else {
13744 arg = 0;
13745 }
13746 return context ? {value: arg} : arg;
13747 };
13748 },
13749 'unary-': function(argument, context) {
13750 return function(scope, locals, assign, inputs) {
13751 var arg = argument(scope, locals, assign, inputs);
13752 if (isDefined(arg)) {
13753 arg = -arg;
13754 } else {
13755 arg = 0;
13756 }
13757 return context ? {value: arg} : arg;
13758 };
13759 },
13760 'unary!': function(argument, context) {
13761 return function(scope, locals, assign, inputs) {
13762 var arg = !argument(scope, locals, assign, inputs);
13763 return context ? {value: arg} : arg;
13764 };
13765 },
13766 'binary+': function(left, right, context) {
13767 return function(scope, locals, assign, inputs) {
13768 var lhs = left(scope, locals, assign, inputs);
13769 var rhs = right(scope, locals, assign, inputs);
13770 var arg = plusFn(lhs, rhs);
13771 return context ? {value: arg} : arg;
13772 };
13773 },
13774 'binary-': function(left, right, context) {
13775 return function(scope, locals, assign, inputs) {
13776 var lhs = left(scope, locals, assign, inputs);
13777 var rhs = right(scope, locals, assign, inputs);
13778 var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0);
13779 return context ? {value: arg} : arg;
13780 };
13781 },
13782 'binary*': function(left, right, context) {
13783 return function(scope, locals, assign, inputs) {
13784 var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs);
13785 return context ? {value: arg} : arg;
13786 };
13787 },
13788 'binary/': function(left, right, context) {
13789 return function(scope, locals, assign, inputs) {
13790 var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs);
13791 return context ? {value: arg} : arg;
13792 };
13793 },
13794 'binary%': function(left, right, context) {
13795 return function(scope, locals, assign, inputs) {
13796 var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs);
13797 return context ? {value: arg} : arg;
13798 };
13799 },
13800 'binary===': function(left, right, context) {
13801 return function(scope, locals, assign, inputs) {
13802 var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs);
13803 return context ? {value: arg} : arg;
13804 };
13805 },
13806 'binary!==': function(left, right, context) {
13807 return function(scope, locals, assign, inputs) {
13808 var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs);
13809 return context ? {value: arg} : arg;
13810 };
13811 },
13812 'binary==': function(left, right, context) {
13813 return function(scope, locals, assign, inputs) {
13814 var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs);
13815 return context ? {value: arg} : arg;
13816 };
13817 },
13818 'binary!=': function(left, right, context) {
13819 return function(scope, locals, assign, inputs) {
13820 var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs);
13821 return context ? {value: arg} : arg;
13822 };
13823 },
13824 'binary<': function(left, right, context) {
13825 return function(scope, locals, assign, inputs) {
13826 var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs);
13827 return context ? {value: arg} : arg;
13828 };
13829 },
13830 'binary>': function(left, right, context) {
13831 return function(scope, locals, assign, inputs) {
13832 var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs);
13833 return context ? {value: arg} : arg;
13834 };
13835 },
13836 'binary<=': function(left, right, context) {
13837 return function(scope, locals, assign, inputs) {
13838 var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs);
13839 return context ? {value: arg} : arg;
13840 };
13841 },
13842 'binary>=': function(left, right, context) {
13843 return function(scope, locals, assign, inputs) {
13844 var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs);
13845 return context ? {value: arg} : arg;
13846 };
13847 },
13848 'binary&&': function(left, right, context) {
13849 return function(scope, locals, assign, inputs) {
13850 var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs);
13851 return context ? {value: arg} : arg;
13852 };
13853 },
13854 'binary||': function(left, right, context) {
13855 return function(scope, locals, assign, inputs) {
13856 var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs);
13857 return context ? {value: arg} : arg;
13858 };
13859 },
13860 'ternary?:': function(test, alternate, consequent, context) {
13861 return function(scope, locals, assign, inputs) {
13862 var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs);
13863 return context ? {value: arg} : arg;
13864 };
13865 },
13866 value: function(value, context) {
13867 return function() { return context ? {context: undefined, name: undefined, value: value} : value; };
13868 },
13869 identifier: function(name, expensiveChecks, context, create, expression) {
13870 return function(scope, locals, assign, inputs) {
13871 var base = locals && (name in locals) ? locals : scope;
13872 if (create && create !== 1 && base && !(base[name])) {
13873 base[name] = {};
13874 }
13875 var value = base ? base[name] : undefined;
13876 if (expensiveChecks) {
13877 ensureSafeObject(value, expression);
13878 }
13879 if (context) {
13880 return {context: base, name: name, value: value};
13881 } else {
13882 return value;
13883 }
13884 };
13885 },
13886 computedMember: function(left, right, context, create, expression) {
13887 return function(scope, locals, assign, inputs) {
13888 var lhs = left(scope, locals, assign, inputs);
13889 var rhs;
13890 var value;
13891 if (lhs != null) {
13892 rhs = right(scope, locals, assign, inputs);
13893 ensureSafeMemberName(rhs, expression);
13894 if (create && create !== 1 && lhs && !(lhs[rhs])) {
13895 lhs[rhs] = {};
13896 }
13897 value = lhs[rhs];
13898 ensureSafeObject(value, expression);
13899 }
13900 if (context) {
13901 return {context: lhs, name: rhs, value: value};
13902 } else {
13903 return value;
13904 }
13905 };
13906 },
13907 nonComputedMember: function(left, right, expensiveChecks, context, create, expression) {
13908 return function(scope, locals, assign, inputs) {
13909 var lhs = left(scope, locals, assign, inputs);
13910 if (create && create !== 1 && lhs && !(lhs[right])) {
13911 lhs[right] = {};
13912 }
13913 var value = lhs != null ? lhs[right] : undefined;
13914 if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
13915 ensureSafeObject(value, expression);
13916 }
13917 if (context) {
13918 return {context: lhs, name: right, value: value};
13919 } else {
13920 return value;
13921 }
13922 };
13923 },
13924 inputs: function(input, watchId) {
13925 return function(scope, value, locals, inputs) {
13926 if (inputs) return inputs[watchId];
13927 return input(scope, value, locals);
13928 };
13929 }
13930 };
13931
13932 /**
13933 * @constructor
13934 */
13935 var Parser = function(lexer, $filter, options) {
13936 this.lexer = lexer;
13937 this.$filter = $filter;
13938 this.options = options;
13939 this.ast = new AST(this.lexer);
13940 this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) :
13941 new ASTCompiler(this.ast, $filter);
13942 };
13943
13944 Parser.prototype = {
13945 constructor: Parser,
13946
13947 parse: function(text) {
13948 return this.astCompiler.compile(text, this.options.expensiveChecks);
13949 }
13950 };
1090813951
1090913952 //////////////////////////////////////////////////
1091013953 // Parser helper functions
1091113954 //////////////////////////////////////////////////
1091213955
10913 function setter(obj, path, setValue, fullExp, options) {
10914 //needed?
10915 options = options || {};
13956 function setter(obj, path, setValue, fullExp) {
13957 ensureSafeObject(obj, fullExp);
1091613958
1091713959 var element = path.split('.'), key;
1091813960 for (var i = 0; element.length > 1; i++) {
1091913961 key = ensureSafeMemberName(element.shift(), fullExp);
10920 var propertyObj = obj[key];
13962 var propertyObj = ensureSafeObject(obj[key], fullExp);
1092113963 if (!propertyObj) {
1092213964 propertyObj = {};
1092313965 obj[key] = propertyObj;
1092413966 }
1092513967 obj = propertyObj;
10926 if (obj.then && options.unwrapPromises) {
10927 promiseWarning(fullExp);
10928 if (!("$$v" in obj)) {
10929 (function(promise) {
10930 promise.then(function(val) { promise.$$v = val; }); }
10931 )(obj);
10932 }
10933 if (obj.$$v === undefined) {
10934 obj.$$v = {};
10935 }
10936 obj = obj.$$v;
10937 }
1093813968 }
1093913969 key = ensureSafeMemberName(element.shift(), fullExp);
10940 ensureSafeObject(obj, fullExp);
1094113970 ensureSafeObject(obj[key], fullExp);
1094213971 obj[key] = setValue;
1094313972 return setValue;
1094413973 }
1094513974
10946 var getterFnCache = {};
10947
10948 /**
10949 * Implementation of the "Black Hole" variant from:
10950 * - http://jsperf.com/angularjs-parse-getter/4
10951 * - http://jsperf.com/path-evaluation-simplified/7
10952 */
10953 function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) {
10954 ensureSafeMemberName(key0, fullExp);
10955 ensureSafeMemberName(key1, fullExp);
10956 ensureSafeMemberName(key2, fullExp);
10957 ensureSafeMemberName(key3, fullExp);
10958 ensureSafeMemberName(key4, fullExp);
10959
10960 return !options.unwrapPromises
10961 ? function cspSafeGetter(scope, locals) {
10962 var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope;
10963
10964 if (pathVal == null) return pathVal;
10965 pathVal = pathVal[key0];
10966
10967 if (!key1) return pathVal;
10968 if (pathVal == null) return undefined;
10969 pathVal = pathVal[key1];
10970
10971 if (!key2) return pathVal;
10972 if (pathVal == null) return undefined;
10973 pathVal = pathVal[key2];
10974
10975 if (!key3) return pathVal;
10976 if (pathVal == null) return undefined;
10977 pathVal = pathVal[key3];
10978
10979 if (!key4) return pathVal;
10980 if (pathVal == null) return undefined;
10981 pathVal = pathVal[key4];
10982
10983 return pathVal;
10984 }
10985 : function cspSafePromiseEnabledGetter(scope, locals) {
10986 var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope,
10987 promise;
10988
10989 if (pathVal == null) return pathVal;
10990
10991 pathVal = pathVal[key0];
10992 if (pathVal && pathVal.then) {
10993 promiseWarning(fullExp);
10994 if (!("$$v" in pathVal)) {
10995 promise = pathVal;
10996 promise.$$v = undefined;
10997 promise.then(function(val) { promise.$$v = val; });
10998 }
10999 pathVal = pathVal.$$v;
11000 }
11001
11002 if (!key1) return pathVal;
11003 if (pathVal == null) return undefined;
11004 pathVal = pathVal[key1];
11005 if (pathVal && pathVal.then) {
11006 promiseWarning(fullExp);
11007 if (!("$$v" in pathVal)) {
11008 promise = pathVal;
11009 promise.$$v = undefined;
11010 promise.then(function(val) { promise.$$v = val; });
11011 }
11012 pathVal = pathVal.$$v;
11013 }
11014
11015 if (!key2) return pathVal;
11016 if (pathVal == null) return undefined;
11017 pathVal = pathVal[key2];
11018 if (pathVal && pathVal.then) {
11019 promiseWarning(fullExp);
11020 if (!("$$v" in pathVal)) {
11021 promise = pathVal;
11022 promise.$$v = undefined;
11023 promise.then(function(val) { promise.$$v = val; });
11024 }
11025 pathVal = pathVal.$$v;
11026 }
11027
11028 if (!key3) return pathVal;
11029 if (pathVal == null) return undefined;
11030 pathVal = pathVal[key3];
11031 if (pathVal && pathVal.then) {
11032 promiseWarning(fullExp);
11033 if (!("$$v" in pathVal)) {
11034 promise = pathVal;
11035 promise.$$v = undefined;
11036 promise.then(function(val) { promise.$$v = val; });
11037 }
11038 pathVal = pathVal.$$v;
11039 }
11040
11041 if (!key4) return pathVal;
11042 if (pathVal == null) return undefined;
11043 pathVal = pathVal[key4];
11044 if (pathVal && pathVal.then) {
11045 promiseWarning(fullExp);
11046 if (!("$$v" in pathVal)) {
11047 promise = pathVal;
11048 promise.$$v = undefined;
11049 promise.then(function(val) { promise.$$v = val; });
11050 }
11051 pathVal = pathVal.$$v;
11052 }
11053 return pathVal;
11054 };
13975 var getterFnCacheDefault = createMap();
13976 var getterFnCacheExpensive = createMap();
13977
13978 function isPossiblyDangerousMemberName(name) {
13979 return name == 'constructor';
1105513980 }
1105613981
11057 function getterFn(path, options, fullExp) {
11058 // Check whether the cache has this getter already.
11059 // We can use hasOwnProperty directly on the cache because we ensure,
11060 // see below, that the cache never stores a path called 'hasOwnProperty'
11061 if (getterFnCache.hasOwnProperty(path)) {
11062 return getterFnCache[path];
11063 }
11064
11065 var pathKeys = path.split('.'),
11066 pathKeysLength = pathKeys.length,
11067 fn;
11068
11069 // http://jsperf.com/angularjs-parse-getter/6
11070 if (options.csp) {
11071 if (pathKeysLength < 6) {
11072 fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp,
11073 options);
11074 } else {
11075 fn = function(scope, locals) {
11076 var i = 0, val;
11077 do {
11078 val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++],
11079 pathKeys[i++], fullExp, options)(scope, locals);
11080
11081 locals = undefined; // clear after first iteration
11082 scope = val;
11083 } while (i < pathKeysLength);
11084 return val;
11085 };
11086 }
11087 } else {
11088 var code = 'var p;\n';
11089 forEach(pathKeys, function(key, index) {
11090 ensureSafeMemberName(key, fullExp);
11091 code += 'if(s == null) return undefined;\n' +
11092 's='+ (index
11093 // we simply dereference 's' on any .dot notation
11094 ? 's'
11095 // but if we are first then we check locals first, and if so read it first
11096 : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
11097 (options.unwrapPromises
11098 ? 'if (s && s.then) {\n' +
11099 ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' +
11100 ' if (!("$$v" in s)) {\n' +
11101 ' p=s;\n' +
11102 ' p.$$v = undefined;\n' +
11103 ' p.then(function(v) {p.$$v=v;});\n' +
11104 '}\n' +
11105 ' s=s.$$v\n' +
11106 '}\n'
11107 : '');
11108 });
11109 code += 'return s;';
11110
11111 /* jshint -W054 */
11112 var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning
11113 /* jshint +W054 */
11114 evaledFnGetter.toString = valueFn(code);
11115 fn = options.unwrapPromises ? function(scope, locals) {
11116 return evaledFnGetter(scope, locals, promiseWarning);
11117 } : evaledFnGetter;
11118 }
11119
11120 // Only cache the value if it's not going to mess up the cache object
11121 // This is more performant that using Object.prototype.hasOwnProperty.call
11122 if (path !== 'hasOwnProperty') {
11123 getterFnCache[path] = fn;
11124 }
11125 return fn;
13982 var objectValueOf = Object.prototype.valueOf;
13983
13984 function getValueOf(value) {
13985 return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value);
1112613986 }
1112713987
1112813988 ///////////////////////////////////
1117114031 /**
1117214032 * @ngdoc provider
1117314033 * @name $parseProvider
11174 * @kind function
1117514034 *
1117614035 * @description
1117714036 * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
1117814037 * service.
1117914038 */
1118014039 function $ParseProvider() {
11181 var cache = {};
11182
11183 var $parseOptions = {
11184 csp: false,
11185 unwrapPromises: false,
11186 logPromiseWarnings: true
11187 };
11188
11189
11190 /**
11191 * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future.
11192 *
11193 * @ngdoc method
11194 * @name $parseProvider#unwrapPromises
11195 * @description
11196 *
11197 * **This feature is deprecated, see deprecation notes below for more info**
11198 *
11199 * If set to true (default is false), $parse will unwrap promises automatically when a promise is
11200 * found at any part of the expression. In other words, if set to true, the expression will always
11201 * result in a non-promise value.
11202 *
11203 * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled,
11204 * the fulfillment value is used in place of the promise while evaluating the expression.
11205 *
11206 * **Deprecation notice**
11207 *
11208 * This is a feature that didn't prove to be wildly useful or popular, primarily because of the
11209 * dichotomy between data access in templates (accessed as raw values) and controller code
11210 * (accessed as promises).
11211 *
11212 * In most code we ended up resolving promises manually in controllers anyway and thus unifying
11213 * the model access there.
11214 *
11215 * Other downsides of automatic promise unwrapping:
11216 *
11217 * - when building components it's often desirable to receive the raw promises
11218 * - adds complexity and slows down expression evaluation
11219 * - makes expression code pre-generation unattractive due to the amount of code that needs to be
11220 * generated
11221 * - makes IDE auto-completion and tool support hard
11222 *
11223 * **Warning Logs**
11224 *
11225 * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a
11226 * promise (to reduce the noise, each expression is logged only once). To disable this logging use
11227 * `$parseProvider.logPromiseWarnings(false)` api.
11228 *
11229 *
11230 * @param {boolean=} value New value.
11231 * @returns {boolean|self} Returns the current setting when used as getter and self if used as
11232 * setter.
11233 */
11234 this.unwrapPromises = function(value) {
11235 if (isDefined(value)) {
11236 $parseOptions.unwrapPromises = !!value;
11237 return this;
11238 } else {
11239 return $parseOptions.unwrapPromises;
11240 }
11241 };
11242
11243
11244 /**
11245 * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future.
11246 *
11247 * @ngdoc method
11248 * @name $parseProvider#logPromiseWarnings
11249 * @description
11250 *
11251 * Controls whether Angular should log a warning on any encounter of a promise in an expression.
11252 *
11253 * The default is set to `true`.
11254 *
11255 * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well.
11256 *
11257 * @param {boolean=} value New value.
11258 * @returns {boolean|self} Returns the current setting when used as getter and self if used as
11259 * setter.
11260 */
11261 this.logPromiseWarnings = function(value) {
11262 if (isDefined(value)) {
11263 $parseOptions.logPromiseWarnings = value;
11264 return this;
11265 } else {
11266 return $parseOptions.logPromiseWarnings;
11267 }
11268 };
11269
11270
11271 this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) {
11272 $parseOptions.csp = $sniffer.csp;
11273
11274 promiseWarning = function promiseWarningFn(fullExp) {
11275 if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return;
11276 promiseWarningCache[fullExp] = true;
11277 $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' +
11278 'Automatic unwrapping of promises in Angular expressions is deprecated.');
11279 };
11280
11281 return function(exp) {
11282 var parsedExpression;
14040 var cacheDefault = createMap();
14041 var cacheExpensive = createMap();
14042
14043 this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
14044 var $parseOptions = {
14045 csp: $sniffer.csp,
14046 expensiveChecks: false
14047 },
14048 $parseOptionsExpensive = {
14049 csp: $sniffer.csp,
14050 expensiveChecks: true
14051 };
14052
14053 return function $parse(exp, interceptorFn, expensiveChecks) {
14054 var parsedExpression, oneTime, cacheKey;
1128314055
1128414056 switch (typeof exp) {
1128514057 case 'string':
11286
11287 if (cache.hasOwnProperty(exp)) {
11288 return cache[exp];
14058 exp = exp.trim();
14059 cacheKey = exp;
14060
14061 var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
14062 parsedExpression = cache[cacheKey];
14063
14064 if (!parsedExpression) {
14065 if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
14066 oneTime = true;
14067 exp = exp.substring(2);
14068 }
14069 var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
14070 var lexer = new Lexer(parseOptions);
14071 var parser = new Parser(lexer, $filter, parseOptions);
14072 parsedExpression = parser.parse(exp);
14073 if (parsedExpression.constant) {
14074 parsedExpression.$$watchDelegate = constantWatchDelegate;
14075 } else if (oneTime) {
14076 parsedExpression.$$watchDelegate = parsedExpression.literal ?
14077 oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
14078 } else if (parsedExpression.inputs) {
14079 parsedExpression.$$watchDelegate = inputsWatchDelegate;
14080 }
14081 cache[cacheKey] = parsedExpression;
1128914082 }
11290
11291 var lexer = new Lexer($parseOptions);
11292 var parser = new Parser(lexer, $filter, $parseOptions);
11293 parsedExpression = parser.parse(exp);
11294
11295 if (exp !== 'hasOwnProperty') {
11296 // Only cache the value if it's not going to mess up the cache object
11297 // This is more performant that using Object.prototype.hasOwnProperty.call
11298 cache[exp] = parsedExpression;
11299 }
11300
11301 return parsedExpression;
14083 return addInterceptor(parsedExpression, interceptorFn);
1130214084
1130314085 case 'function':
11304 return exp;
14086 return addInterceptor(exp, interceptorFn);
1130514087
1130614088 default:
1130714089 return noop;
1130814090 }
1130914091 };
14092
14093 function expressionInputDirtyCheck(newValue, oldValueOfValue) {
14094
14095 if (newValue == null || oldValueOfValue == null) { // null/undefined
14096 return newValue === oldValueOfValue;
14097 }
14098
14099 if (typeof newValue === 'object') {
14100
14101 // attempt to convert the value to a primitive type
14102 // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
14103 // be cheaply dirty-checked
14104 newValue = getValueOf(newValue);
14105
14106 if (typeof newValue === 'object') {
14107 // objects/arrays are not supported - deep-watching them would be too expensive
14108 return false;
14109 }
14110
14111 // fall-through to the primitive equality check
14112 }
14113
14114 //Primitive or NaN
14115 return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue);
14116 }
14117
14118 function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
14119 var inputExpressions = parsedExpression.inputs;
14120 var lastResult;
14121
14122 if (inputExpressions.length === 1) {
14123 var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails
14124 inputExpressions = inputExpressions[0];
14125 return scope.$watch(function expressionInputWatch(scope) {
14126 var newInputValue = inputExpressions(scope);
14127 if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) {
14128 lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
14129 oldInputValueOf = newInputValue && getValueOf(newInputValue);
14130 }
14131 return lastResult;
14132 }, listener, objectEquality, prettyPrintExpression);
14133 }
14134
14135 var oldInputValueOfValues = [];
14136 var oldInputValues = [];
14137 for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
14138 oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails
14139 oldInputValues[i] = null;
14140 }
14141
14142 return scope.$watch(function expressionInputsWatch(scope) {
14143 var changed = false;
14144
14145 for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
14146 var newInputValue = inputExpressions[i](scope);
14147 if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) {
14148 oldInputValues[i] = newInputValue;
14149 oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
14150 }
14151 }
14152
14153 if (changed) {
14154 lastResult = parsedExpression(scope, undefined, undefined, oldInputValues);
14155 }
14156
14157 return lastResult;
14158 }, listener, objectEquality, prettyPrintExpression);
14159 }
14160
14161 function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
14162 var unwatch, lastValue;
14163 return unwatch = scope.$watch(function oneTimeWatch(scope) {
14164 return parsedExpression(scope);
14165 }, function oneTimeListener(value, old, scope) {
14166 lastValue = value;
14167 if (isFunction(listener)) {
14168 listener.apply(this, arguments);
14169 }
14170 if (isDefined(value)) {
14171 scope.$$postDigest(function() {
14172 if (isDefined(lastValue)) {
14173 unwatch();
14174 }
14175 });
14176 }
14177 }, objectEquality);
14178 }
14179
14180 function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
14181 var unwatch, lastValue;
14182 return unwatch = scope.$watch(function oneTimeWatch(scope) {
14183 return parsedExpression(scope);
14184 }, function oneTimeListener(value, old, scope) {
14185 lastValue = value;
14186 if (isFunction(listener)) {
14187 listener.call(this, value, old, scope);
14188 }
14189 if (isAllDefined(value)) {
14190 scope.$$postDigest(function() {
14191 if (isAllDefined(lastValue)) unwatch();
14192 });
14193 }
14194 }, objectEquality);
14195
14196 function isAllDefined(value) {
14197 var allDefined = true;
14198 forEach(value, function(val) {
14199 if (!isDefined(val)) allDefined = false;
14200 });
14201 return allDefined;
14202 }
14203 }
14204
14205 function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
14206 var unwatch;
14207 return unwatch = scope.$watch(function constantWatch(scope) {
14208 return parsedExpression(scope);
14209 }, function constantListener(value, old, scope) {
14210 if (isFunction(listener)) {
14211 listener.apply(this, arguments);
14212 }
14213 unwatch();
14214 }, objectEquality);
14215 }
14216
14217 function addInterceptor(parsedExpression, interceptorFn) {
14218 if (!interceptorFn) return parsedExpression;
14219 var watchDelegate = parsedExpression.$$watchDelegate;
14220
14221 var regularWatch =
14222 watchDelegate !== oneTimeLiteralWatchDelegate &&
14223 watchDelegate !== oneTimeWatchDelegate;
14224
14225 var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
14226 var value = parsedExpression(scope, locals, assign, inputs);
14227 return interceptorFn(value, scope, locals);
14228 } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
14229 var value = parsedExpression(scope, locals, assign, inputs);
14230 var result = interceptorFn(value, scope, locals);
14231 // we only return the interceptor's result if the
14232 // initial value is defined (for bind-once)
14233 return isDefined(value) ? result : value;
14234 };
14235
14236 // Propagate $$watchDelegates other then inputsWatchDelegate
14237 if (parsedExpression.$$watchDelegate &&
14238 parsedExpression.$$watchDelegate !== inputsWatchDelegate) {
14239 fn.$$watchDelegate = parsedExpression.$$watchDelegate;
14240 } else if (!interceptorFn.$stateful) {
14241 // If there is an interceptor, but no watchDelegate then treat the interceptor like
14242 // we treat filters - it is assumed to be a pure function unless flagged with $stateful
14243 fn.$$watchDelegate = inputsWatchDelegate;
14244 fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
14245 }
14246
14247 return fn;
14248 }
1131014249 }];
1131114250 }
1131214251
1131614255 * @requires $rootScope
1131714256 *
1131814257 * @description
11319 * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
14258 * A service that helps you run functions asynchronously, and use their return values (or exceptions)
14259 * when they are done processing.
14260 *
14261 * This is an implementation of promises/deferred objects inspired by
14262 * [Kris Kowal's Q](https://github.com/kriskowal/q).
14263 *
14264 * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
14265 * implementations, and the other which resembles ES6 promises to some degree.
14266 *
14267 * # $q constructor
14268 *
14269 * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
14270 * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony,
14271 * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
14272 *
14273 * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are
14274 * available yet.
14275 *
14276 * It can be used like so:
14277 *
14278 * ```js
14279 * // for the purpose of this example let's assume that variables `$q` and `okToGreet`
14280 * // are available in the current lexical scope (they could have been injected or passed in).
14281 *
14282 * function asyncGreet(name) {
14283 * // perform some asynchronous operation, resolve or reject the promise when appropriate.
14284 * return $q(function(resolve, reject) {
14285 * setTimeout(function() {
14286 * if (okToGreet(name)) {
14287 * resolve('Hello, ' + name + '!');
14288 * } else {
14289 * reject('Greeting ' + name + ' is not allowed.');
14290 * }
14291 * }, 1000);
14292 * });
14293 * }
14294 *
14295 * var promise = asyncGreet('Robin Hood');
14296 * promise.then(function(greeting) {
14297 * alert('Success: ' + greeting);
14298 * }, function(reason) {
14299 * alert('Failed: ' + reason);
14300 * });
14301 * ```
14302 *
14303 * Note: progress/notify callbacks are not currently supported via the ES6-style interface.
14304 *
14305 * However, the more traditional CommonJS-style usage is still available, and documented below.
1132014306 *
1132114307 * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
1132214308 * interface for interacting with an object that represents the result of an action that is
1132614312 * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming.
1132714313 *
1132814314 * ```js
11329 * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet`
14315 * // for the purpose of this example let's assume that variables `$q` and `okToGreet`
1133014316 * // are available in the current lexical scope (they could have been injected or passed in).
1133114317 *
1133214318 * function asyncGreet(name) {
1136414350 * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the
1136514351 * section on serial or parallel joining of promises.
1136614352 *
11367 *
1136814353 * # The Deferred API
1136914354 *
1137014355 * A new instance of deferred is constructed by calling `$q.defer()`.
1140414389 * provide a progress indication, before the promise is resolved or rejected.
1140514390 *
1140614391 * This method *returns a new promise* which is resolved or rejected via the return value of the
11407 * `successCallback`, `errorCallback`. It also notifies via the return value of the
11408 * `notifyCallback` method. The promise can not be resolved or rejected from the notifyCallback
11409 * method.
14392 * `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved
14393 * with the value which is resolved in that promise using
14394 * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)).
14395 * It also notifies via the return value of the `notifyCallback` method. The promise cannot be
14396 * resolved or rejected from the notifyCallback method.
1141014397 *
1141114398 * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
1141214399 *
11413 * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise,
14400 * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise,
1141414401 * but to do so without modifying the final value. This is useful to release resources or do some
1141514402 * clean-up that needs to be done whether the promise was rejected or resolved. See the [full
1141614403 * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for
1141714404 * more information.
11418 *
11419 * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as
11420 * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to
11421 * make your code IE8 and Android 2.x compatible.
1142214405 *
1142314406 * # Chaining promises
1142414407 *
1147314456 * expect(resolvedValue).toEqual(123);
1147414457 * }));
1147514458 * ```
14459 *
14460 * @param {function(function, function)} resolver Function which is responsible for resolving or
14461 * rejecting the newly created promise. The first parameter is a function which resolves the
14462 * promise, the second parameter is a function which rejects the promise.
14463 *
14464 * @returns {Promise} The newly created promise.
1147614465 */
1147714466 function $QProvider() {
1147814467
1148314472 }];
1148414473 }
1148514474
14475 function $$QProvider() {
14476 this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
14477 return qFactory(function(callback) {
14478 $browser.defer(callback);
14479 }, $exceptionHandler);
14480 }];
14481 }
1148614482
1148714483 /**
1148814484 * Constructs a promise manager.
1148914485 *
11490 * @param {function(Function)} nextTick Function for executing functions in the next turn.
14486 * @param {function(function)} nextTick Function for executing functions in the next turn.
1149114487 * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
1149214488 * debugging purposes.
1149314489 * @returns {object} Promise manager.
1149414490 */
1149514491 function qFactory(nextTick, exceptionHandler) {
14492 var $qMinErr = minErr('$q', TypeError);
14493 function callOnce(self, resolveFn, rejectFn) {
14494 var called = false;
14495 function wrap(fn) {
14496 return function(value) {
14497 if (called) return;
14498 called = true;
14499 fn.call(self, value);
14500 };
14501 }
14502
14503 return [wrap(resolveFn), wrap(rejectFn)];
14504 }
1149614505
1149714506 /**
1149814507 * @ngdoc method
11499 * @name $q#defer
14508 * @name ng.$q#defer
1150014509 * @kind function
1150114510 *
1150214511 * @description
1150514514 * @returns {Deferred} Returns a new instance of deferred.
1150614515 */
1150714516 var defer = function() {
11508 var pending = [],
11509 value, deferred;
11510
11511 deferred = {
11512
11513 resolve: function(val) {
11514 if (pending) {
11515 var callbacks = pending;
11516 pending = undefined;
11517 value = ref(val);
11518
11519 if (callbacks.length) {
11520 nextTick(function() {
11521 var callback;
11522 for (var i = 0, ii = callbacks.length; i < ii; i++) {
11523 callback = callbacks[i];
11524 value.then(callback[0], callback[1], callback[2]);
11525 }
11526 });
11527 }
14517 return new Deferred();
14518 };
14519
14520 function Promise() {
14521 this.$$state = { status: 0 };
14522 }
14523
14524 Promise.prototype = {
14525 then: function(onFulfilled, onRejected, progressBack) {
14526 var result = new Deferred();
14527
14528 this.$$state.pending = this.$$state.pending || [];
14529 this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
14530 if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
14531
14532 return result.promise;
14533 },
14534
14535 "catch": function(callback) {
14536 return this.then(null, callback);
14537 },
14538
14539 "finally": function(callback, progressBack) {
14540 return this.then(function(value) {
14541 return handleCallback(value, true, callback);
14542 }, function(error) {
14543 return handleCallback(error, false, callback);
14544 }, progressBack);
14545 }
14546 };
14547
14548 //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native
14549 function simpleBind(context, fn) {
14550 return function(value) {
14551 fn.call(context, value);
14552 };
14553 }
14554
14555 function processQueue(state) {
14556 var fn, deferred, pending;
14557
14558 pending = state.pending;
14559 state.processScheduled = false;
14560 state.pending = undefined;
14561 for (var i = 0, ii = pending.length; i < ii; ++i) {
14562 deferred = pending[i][0];
14563 fn = pending[i][state.status];
14564 try {
14565 if (isFunction(fn)) {
14566 deferred.resolve(fn(state.value));
14567 } else if (state.status === 1) {
14568 deferred.resolve(state.value);
14569 } else {
14570 deferred.reject(state.value);
1152814571 }
11529 },
11530
11531
11532 reject: function(reason) {
11533 deferred.resolve(createInternalRejectedPromise(reason));
11534 },
11535
11536
11537 notify: function(progress) {
11538 if (pending) {
11539 var callbacks = pending;
11540
11541 if (pending.length) {
11542 nextTick(function() {
11543 var callback;
11544 for (var i = 0, ii = callbacks.length; i < ii; i++) {
11545 callback = callbacks[i];
11546 callback[2](progress);
11547 }
11548 });
11549 }
14572 } catch (e) {
14573 deferred.reject(e);
14574 exceptionHandler(e);
14575 }
14576 }
14577 }
14578
14579 function scheduleProcessQueue(state) {
14580 if (state.processScheduled || !state.pending) return;
14581 state.processScheduled = true;
14582 nextTick(function() { processQueue(state); });
14583 }
14584
14585 function Deferred() {
14586 this.promise = new Promise();
14587 //Necessary to support unbound execution :/
14588 this.resolve = simpleBind(this, this.resolve);
14589 this.reject = simpleBind(this, this.reject);
14590 this.notify = simpleBind(this, this.notify);
14591 }
14592
14593 Deferred.prototype = {
14594 resolve: function(val) {
14595 if (this.promise.$$state.status) return;
14596 if (val === this.promise) {
14597 this.$$reject($qMinErr(
14598 'qcycle',
14599 "Expected promise to be resolved with value other than itself '{0}'",
14600 val));
14601 } else {
14602 this.$$resolve(val);
14603 }
14604
14605 },
14606
14607 $$resolve: function(val) {
14608 var then, fns;
14609
14610 fns = callOnce(this, this.$$resolve, this.$$reject);
14611 try {
14612 if ((isObject(val) || isFunction(val))) then = val && val.then;
14613 if (isFunction(then)) {
14614 this.promise.$$state.status = -1;
14615 then.call(val, fns[0], fns[1], this.notify);
14616 } else {
14617 this.promise.$$state.value = val;
14618 this.promise.$$state.status = 1;
14619 scheduleProcessQueue(this.promise.$$state);
1155014620 }
11551 },
11552
11553
11554 promise: {
11555 then: function(callback, errback, progressback) {
11556 var result = defer();
11557
11558 var wrappedCallback = function(value) {
14621 } catch (e) {
14622 fns[1](e);
14623 exceptionHandler(e);
14624 }
14625 },
14626
14627 reject: function(reason) {
14628 if (this.promise.$$state.status) return;
14629 this.$$reject(reason);
14630 },
14631
14632 $$reject: function(reason) {
14633 this.promise.$$state.value = reason;
14634 this.promise.$$state.status = 2;
14635 scheduleProcessQueue(this.promise.$$state);
14636 },
14637
14638 notify: function(progress) {
14639 var callbacks = this.promise.$$state.pending;
14640
14641 if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) {
14642 nextTick(function() {
14643 var callback, result;
14644 for (var i = 0, ii = callbacks.length; i < ii; i++) {
14645 result = callbacks[i][0];
14646 callback = callbacks[i][3];
1155914647 try {
11560 result.resolve((isFunction(callback) ? callback : defaultCallback)(value));
11561 } catch(e) {
11562 result.reject(e);
14648 result.notify(isFunction(callback) ? callback(progress) : progress);
14649 } catch (e) {
1156314650 exceptionHandler(e);
1156414651 }
11565 };
11566
11567 var wrappedErrback = function(reason) {
11568 try {
11569 result.resolve((isFunction(errback) ? errback : defaultErrback)(reason));
11570 } catch(e) {
11571 result.reject(e);
11572 exceptionHandler(e);
11573 }
11574 };
11575
11576 var wrappedProgressback = function(progress) {
11577 try {
11578 result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress));
11579 } catch(e) {
11580 exceptionHandler(e);
11581 }
11582 };
11583
11584 if (pending) {
11585 pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]);
11586 } else {
11587 value.then(wrappedCallback, wrappedErrback, wrappedProgressback);
1158814652 }
11589
11590 return result.promise;
11591 },
11592
11593 "catch": function(callback) {
11594 return this.then(null, callback);
11595 },
11596
11597 "finally": function(callback) {
11598
11599 function makePromise(value, resolved) {
11600 var result = defer();
11601 if (resolved) {
11602 result.resolve(value);
11603 } else {
11604 result.reject(value);
11605 }
11606 return result.promise;
11607 }
11608
11609 function handleCallback(value, isResolved) {
11610 var callbackOutput = null;
11611 try {
11612 callbackOutput = (callback ||defaultCallback)();
11613 } catch(e) {
11614 return makePromise(e, false);
11615 }
11616 if (isPromiseLike(callbackOutput)) {
11617 return callbackOutput.then(function() {
11618 return makePromise(value, isResolved);
11619 }, function(error) {
11620 return makePromise(error, false);
11621 });
11622 } else {
11623 return makePromise(value, isResolved);
11624 }
11625 }
11626
11627 return this.then(function(value) {
11628 return handleCallback(value, true);
11629 }, function(error) {
11630 return handleCallback(error, false);
11631 });
11632 }
11633 }
11634 };
11635
11636 return deferred;
14653 });
14654 }
14655 }
1163714656 };
11638
11639
11640 var ref = function(value) {
11641 if (isPromiseLike(value)) return value;
11642 return {
11643 then: function(callback) {
11644 var result = defer();
11645 nextTick(function() {
11646 result.resolve(callback(value));
11647 });
11648 return result.promise;
11649 }
11650 };
11651 };
11652
1165314657
1165414658 /**
1165514659 * @ngdoc method
1168814692 * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
1168914693 */
1169014694 var reject = function(reason) {
11691 var result = defer();
14695 var result = new Deferred();
1169214696 result.reject(reason);
1169314697 return result.promise;
1169414698 };
1169514699
11696 var createInternalRejectedPromise = function(reason) {
11697 return {
11698 then: function(callback, errback) {
11699 var result = defer();
11700 nextTick(function() {
11701 try {
11702 result.resolve((isFunction(errback) ? errback : defaultErrback)(reason));
11703 } catch(e) {
11704 result.reject(e);
11705 exceptionHandler(e);
11706 }
11707 });
11708 return result.promise;
11709 }
11710 };
14700 var makePromise = function makePromise(value, resolved) {
14701 var result = new Deferred();
14702 if (resolved) {
14703 result.resolve(value);
14704 } else {
14705 result.reject(value);
14706 }
14707 return result.promise;
1171114708 };
1171214709
14710 var handleCallback = function handleCallback(value, isResolved, callback) {
14711 var callbackOutput = null;
14712 try {
14713 if (isFunction(callback)) callbackOutput = callback();
14714 } catch (e) {
14715 return makePromise(e, false);
14716 }
14717 if (isPromiseLike(callbackOutput)) {
14718 return callbackOutput.then(function() {
14719 return makePromise(value, isResolved);
14720 }, function(error) {
14721 return makePromise(error, false);
14722 });
14723 } else {
14724 return makePromise(value, isResolved);
14725 }
14726 };
1171314727
1171414728 /**
1171514729 * @ngdoc method
1172414738 * @param {*} value Value or a promise
1172514739 * @returns {Promise} Returns a promise of the passed value or promise
1172614740 */
11727 var when = function(value, callback, errback, progressback) {
11728 var result = defer(),
11729 done;
11730
11731 var wrappedCallback = function(value) {
11732 try {
11733 return (isFunction(callback) ? callback : defaultCallback)(value);
11734 } catch (e) {
11735 exceptionHandler(e);
11736 return reject(e);
11737 }
11738 };
11739
11740 var wrappedErrback = function(reason) {
11741 try {
11742 return (isFunction(errback) ? errback : defaultErrback)(reason);
11743 } catch (e) {
11744 exceptionHandler(e);
11745 return reject(e);
11746 }
11747 };
11748
11749 var wrappedProgressback = function(progress) {
11750 try {
11751 return (isFunction(progressback) ? progressback : defaultCallback)(progress);
11752 } catch (e) {
11753 exceptionHandler(e);
11754 }
11755 };
11756
11757 nextTick(function() {
11758 ref(value).then(function(value) {
11759 if (done) return;
11760 done = true;
11761 result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback));
11762 }, function(reason) {
11763 if (done) return;
11764 done = true;
11765 result.resolve(wrappedErrback(reason));
11766 }, function(progress) {
11767 if (done) return;
11768 result.notify(wrappedProgressback(progress));
11769 });
11770 });
11771
11772 return result.promise;
14741
14742
14743 var when = function(value, callback, errback, progressBack) {
14744 var result = new Deferred();
14745 result.resolve(value);
14746 return result.promise.then(callback, errback, progressBack);
1177314747 };
1177414748
11775
11776 function defaultCallback(value) {
11777 return value;
11778 }
11779
11780
11781 function defaultErrback(reason) {
11782 return reject(reason);
11783 }
11784
14749 /**
14750 * @ngdoc method
14751 * @name $q#resolve
14752 * @kind function
14753 *
14754 * @description
14755 * Alias of {@link ng.$q#when when} to maintain naming consistency with ES6.
14756 *
14757 * @param {*} value Value or a promise
14758 * @returns {Promise} Returns a promise of the passed value or promise
14759 */
14760 var resolve = when;
1178514761
1178614762 /**
1178714763 * @ngdoc method
1179814774 * If any of the promises is resolved with a rejection, this resulting promise will be rejected
1179914775 * with the same rejection value.
1180014776 */
14777
1180114778 function all(promises) {
11802 var deferred = defer(),
14779 var deferred = new Deferred(),
1180314780 counter = 0,
1180414781 results = isArray(promises) ? [] : {};
1180514782
1180614783 forEach(promises, function(promise, key) {
1180714784 counter++;
11808 ref(promise).then(function(value) {
14785 when(promise).then(function(value) {
1180914786 if (results.hasOwnProperty(key)) return;
1181014787 results[key] = value;
1181114788 if (!(--counter)) deferred.resolve(results);
1182214799 return deferred.promise;
1182314800 }
1182414801
11825 return {
11826 defer: defer,
11827 reject: reject,
11828 when: when,
11829 all: all
14802 var $Q = function Q(resolver) {
14803 if (!isFunction(resolver)) {
14804 throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
14805 }
14806
14807 if (!(this instanceof Q)) {
14808 // More useful when $Q is the Promise itself.
14809 return new Q(resolver);
14810 }
14811
14812 var deferred = new Deferred();
14813
14814 function resolveFn(value) {
14815 deferred.resolve(value);
14816 }
14817
14818 function rejectFn(reason) {
14819 deferred.reject(reason);
14820 }
14821
14822 resolver(resolveFn, rejectFn);
14823
14824 return deferred.promise;
1183014825 };
14826
14827 $Q.defer = defer;
14828 $Q.reject = reject;
14829 $Q.when = when;
14830 $Q.resolve = resolve;
14831 $Q.all = all;
14832
14833 return $Q;
1183114834 }
1183214835
11833 function $$RAFProvider(){ //rAF
14836 function $$RAFProvider() { //rAF
1183414837 this.$get = ['$window', '$timeout', function($window, $timeout) {
1183514838 var requestAnimationFrame = $window.requestAnimationFrame ||
11836 $window.webkitRequestAnimationFrame ||
11837 $window.mozRequestAnimationFrame;
14839 $window.webkitRequestAnimationFrame;
1183814840
1183914841 var cancelAnimationFrame = $window.cancelAnimationFrame ||
1184014842 $window.webkitCancelAnimationFrame ||
11841 $window.mozCancelAnimationFrame ||
1184214843 $window.webkitCancelRequestAnimationFrame;
1184314844
1184414845 var rafSupported = !!requestAnimationFrame;
11845 var raf = rafSupported
14846 var rafFn = rafSupported
1184614847 ? function(fn) {
1184714848 var id = requestAnimationFrame(fn);
1184814849 return function() {
1185614857 };
1185714858 };
1185814859
11859 raf.supported = rafSupported;
11860
11861 return raf;
14860 queueFn.supported = rafSupported;
14861
14862 var cancelLastRAF;
14863 var taskCount = 0;
14864 var taskQueue = [];
14865 return queueFn;
14866
14867 function flush() {
14868 for (var i = 0; i < taskQueue.length; i++) {
14869 var task = taskQueue[i];
14870 if (task) {
14871 taskQueue[i] = null;
14872 task();
14873 }
14874 }
14875 taskCount = taskQueue.length = 0;
14876 }
14877
14878 function queueFn(asyncFn) {
14879 var index = taskQueue.length;
14880
14881 taskCount++;
14882 taskQueue.push(asyncFn);
14883
14884 if (index === 0) {
14885 cancelLastRAF = rafFn(flush);
14886 }
14887
14888 return function cancelQueueFn() {
14889 if (index >= 0) {
14890 taskQueue[index] = null;
14891 index = null;
14892
14893 if (--taskCount === 0 && cancelLastRAF) {
14894 cancelLastRAF();
14895 cancelLastRAF = null;
14896 taskQueue.length = 0;
14897 }
14898 }
14899 };
14900 }
1186214901 }];
1186314902 }
1186414903
1192914968 * They also provide an event emission/broadcast and subscription facility. See the
1193014969 * {@link guide/scope developer guide on scopes}.
1193114970 */
11932 function $RootScopeProvider(){
14971 function $RootScopeProvider() {
1193314972 var TTL = 10;
1193414973 var $rootScopeMinErr = minErr('$rootScope');
1193514974 var lastDirtyWatch = null;
14975 var applyAsyncId = null;
1193614976
1193714977 this.digestTtl = function(value) {
1193814978 if (arguments.length) {
1194114981 return TTL;
1194214982 };
1194314983
14984 function createChildScopeClass(parent) {
14985 function ChildScope() {
14986 this.$$watchers = this.$$nextSibling =
14987 this.$$childHead = this.$$childTail = null;
14988 this.$$listeners = {};
14989 this.$$listenerCount = {};
14990 this.$$watchersCount = 0;
14991 this.$id = nextUid();
14992 this.$$ChildScope = null;
14993 }
14994 ChildScope.prototype = parent;
14995 return ChildScope;
14996 }
14997
1194414998 this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
11945 function( $injector, $exceptionHandler, $parse, $browser) {
14999 function($injector, $exceptionHandler, $parse, $browser) {
15000
15001 function destroyChildScope($event) {
15002 $event.currentScope.$$destroyed = true;
15003 }
1194615004
1194715005 /**
1194815006 * @ngdoc type
1196615024 var child = parent.$new();
1196715025
1196815026 parent.salutation = "Hello";
11969 child.name = "World";
1197015027 expect(child.salutation).toEqual('Hello');
1197115028
1197215029 child.salutation = "Welcome";
1197315030 expect(child.salutation).toEqual('Welcome');
1197415031 expect(parent.salutation).toEqual('Hello');
1197515032 * ```
15033 *
15034 * When interacting with `Scope` in tests, additional helper methods are available on the
15035 * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional
15036 * details.
1197615037 *
1197715038 *
1197815039 * @param {Object.<string, function()>=} providers Map of service factory which need to be
1198915050 this.$$phase = this.$parent = this.$$watchers =
1199015051 this.$$nextSibling = this.$$prevSibling =
1199115052 this.$$childHead = this.$$childTail = null;
11992 this['this'] = this.$root = this;
15053 this.$root = this;
1199315054 this.$$destroyed = false;
11994 this.$$asyncQueue = [];
11995 this.$$postDigestQueue = [];
1199615055 this.$$listeners = {};
1199715056 this.$$listenerCount = {};
11998 this.$$isolateBindings = {};
15057 this.$$watchersCount = 0;
15058 this.$$isolateBindings = null;
1199915059 }
1200015060
1200115061 /**
1200215062 * @ngdoc property
1200315063 * @name $rootScope.Scope#$id
12004 * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for
12005 * debugging.
15064 *
15065 * @description
15066 * Unique scope ID (monotonically increasing) useful for debugging.
1200615067 */
1200715068
15069 /**
15070 * @ngdoc property
15071 * @name $rootScope.Scope#$parent
15072 *
15073 * @description
15074 * Reference to the parent scope.
15075 */
15076
15077 /**
15078 * @ngdoc property
15079 * @name $rootScope.Scope#$root
15080 *
15081 * @description
15082 * Reference to the root scope.
15083 */
1200815084
1200915085 Scope.prototype = {
1201015086 constructor: Scope,
1201615092 * @description
1201715093 * Creates a new child {@link ng.$rootScope.Scope scope}.
1201815094 *
12019 * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and
12020 * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the
12021 * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}.
15095 * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event.
15096 * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}.
1202215097 *
1202315098 * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is
1202415099 * desired for the scope and its child scopes to be permanently detached from the parent and
1202915104 * When creating widgets, it is useful for the widget to not accidentally read parent
1203015105 * state.
1203115106 *
15107 * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent`
15108 * of the newly created scope. Defaults to `this` scope if not provided.
15109 * This is used when creating a transclude scope to correctly place it
15110 * in the scope hierarchy while maintaining the correct prototypical
15111 * inheritance.
15112 *
1203215113 * @returns {Object} The newly created child scope.
1203315114 *
1203415115 */
12035 $new: function(isolate) {
12036 var ChildScope,
12037 child;
15116 $new: function(isolate, parent) {
15117 var child;
15118
15119 parent = parent || this;
1203815120
1203915121 if (isolate) {
1204015122 child = new Scope();
1204115123 child.$root = this.$root;
12042 // ensure that there is just one async queue per $rootScope and its children
12043 child.$$asyncQueue = this.$$asyncQueue;
12044 child.$$postDigestQueue = this.$$postDigestQueue;
1204515124 } else {
1204615125 // Only create a child scope class if somebody asks for one,
1204715126 // but cache it to allow the VM to optimize lookups.
12048 if (!this.$$childScopeClass) {
12049 this.$$childScopeClass = function() {
12050 this.$$watchers = this.$$nextSibling =
12051 this.$$childHead = this.$$childTail = null;
12052 this.$$listeners = {};
12053 this.$$listenerCount = {};
12054 this.$id = nextUid();
12055 this.$$childScopeClass = null;
12056 };
12057 this.$$childScopeClass.prototype = this;
15127 if (!this.$$ChildScope) {
15128 this.$$ChildScope = createChildScopeClass(this);
1205815129 }
12059 child = new this.$$childScopeClass();
15130 child = new this.$$ChildScope();
1206015131 }
12061 child['this'] = child;
12062 child.$parent = this;
12063 child.$$prevSibling = this.$$childTail;
12064 if (this.$$childHead) {
12065 this.$$childTail.$$nextSibling = child;
12066 this.$$childTail = child;
15132 child.$parent = parent;
15133 child.$$prevSibling = parent.$$childTail;
15134 if (parent.$$childHead) {
15135 parent.$$childTail.$$nextSibling = child;
15136 parent.$$childTail = child;
1206715137 } else {
12068 this.$$childHead = this.$$childTail = child;
15138 parent.$$childHead = parent.$$childTail = child;
1206915139 }
15140
15141 // When the new scope is not isolated or we inherit from `this`, and
15142 // the parent scope is destroyed, the property `$$destroyed` is inherited
15143 // prototypically. In all other cases, this property needs to be set
15144 // when the parent scope is destroyed.
15145 // The listener needs to be added after the parent is set
15146 if (isolate || parent != this) child.$on('$destroy', destroyChildScope);
15147
1207015148 return child;
1207115149 },
1207215150
1211015188 * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the
1211115189 * listener was called due to initialization.
1211215190 *
12113 * The example below contains an illustration of using a function as your $watch listener
1211415191 *
1211515192 *
1211615193 * # Example
1214015217
1214115218
1214215219
12143 // Using a listener function
15220 // Using a function as a watchExpression
1214415221 var food;
1214515222 scope.foodCounter = 0;
1214615223 expect(scope.foodCounter).toEqual(0);
1214715224 scope.$watch(
12148 // This is the listener function
15225 // This function returns the value being watched. It is called for each turn of the $digest loop
1214915226 function() { return food; },
12150 // This is the change handler
15227 // This is the change listener, called when the value returned from the above function changes
1215115228 function(newValue, oldValue) {
1215215229 if ( newValue !== oldValue ) {
1215315230 // Only increment the counter if the value changed
1217715254 *
1217815255 * - `string`: Evaluated as {@link guide/expression expression}
1217915256 * - `function(scope)`: called with current `scope` as a parameter.
12180 * @param {(function()|string)=} listener Callback called whenever the return value of
12181 * the `watchExpression` changes.
15257 * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value
15258 * of `watchExpression` changes.
1218215259 *
12183 * - `string`: Evaluated as {@link guide/expression expression}
12184 * - `function(newValue, oldValue, scope)`: called with current and previous values as
12185 * parameters.
12186 *
15260 * - `newVal` contains the current value of the `watchExpression`
15261 * - `oldVal` contains the previous value of the `watchExpression`
15262 * - `scope` refers to the current scope
1218715263 * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of
1218815264 * comparing for reference equality.
1218915265 * @returns {function()} Returns a deregistration function for this listener.
1219015266 */
12191 $watch: function(watchExp, listener, objectEquality) {
15267 $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
15268 var get = $parse(watchExp);
15269
15270 if (get.$$watchDelegate) {
15271 return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
15272 }
1219215273 var scope = this,
12193 get = compileToFn(watchExp, 'watch'),
1219415274 array = scope.$$watchers,
1219515275 watcher = {
1219615276 fn: listener,
1219715277 last: initWatchVal,
1219815278 get: get,
12199 exp: watchExp,
15279 exp: prettyPrintExpression || watchExp,
1220015280 eq: !!objectEquality
1220115281 };
1220215282
1220315283 lastDirtyWatch = null;
1220415284
12205 // in the case user pass string, we need to compile it, do we really need this ?
1220615285 if (!isFunction(listener)) {
12207 var listenFn = compileToFn(listener || noop, 'listener');
12208 watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
12209 }
12210
12211 if (typeof watchExp == 'string' && get.constant) {
12212 var originalFn = watcher.fn;
12213 watcher.fn = function(newVal, oldVal, scope) {
12214 originalFn.call(this, newVal, oldVal, scope);
12215 arrayRemove(array, watcher);
12216 };
15286 watcher.fn = noop;
1221715287 }
1221815288
1221915289 if (!array) {
1222215292 // we use unshift since we use a while loop in $digest for speed.
1222315293 // the while loop reads in reverse order.
1222415294 array.unshift(watcher);
15295 incrementWatchersCount(this, 1);
1222515296
1222615297 return function deregisterWatch() {
12227 arrayRemove(array, watcher);
15298 if (arrayRemove(array, watcher) >= 0) {
15299 incrementWatchersCount(scope, -1);
15300 }
1222815301 lastDirtyWatch = null;
15302 };
15303 },
15304
15305 /**
15306 * @ngdoc method
15307 * @name $rootScope.Scope#$watchGroup
15308 * @kind function
15309 *
15310 * @description
15311 * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`.
15312 * If any one expression in the collection changes the `listener` is executed.
15313 *
15314 * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every
15315 * call to $digest() to see if any items changes.
15316 * - The `listener` is called whenever any expression in the `watchExpressions` array changes.
15317 *
15318 * @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually
15319 * watched using {@link ng.$rootScope.Scope#$watch $watch()}
15320 *
15321 * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any
15322 * expression in `watchExpressions` changes
15323 * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching
15324 * those of `watchExpression`
15325 * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching
15326 * those of `watchExpression`
15327 * The `scope` refers to the current scope.
15328 * @returns {function()} Returns a de-registration function for all listeners.
15329 */
15330 $watchGroup: function(watchExpressions, listener) {
15331 var oldValues = new Array(watchExpressions.length);
15332 var newValues = new Array(watchExpressions.length);
15333 var deregisterFns = [];
15334 var self = this;
15335 var changeReactionScheduled = false;
15336 var firstRun = true;
15337
15338 if (!watchExpressions.length) {
15339 // No expressions means we call the listener ASAP
15340 var shouldCall = true;
15341 self.$evalAsync(function() {
15342 if (shouldCall) listener(newValues, newValues, self);
15343 });
15344 return function deregisterWatchGroup() {
15345 shouldCall = false;
15346 };
15347 }
15348
15349 if (watchExpressions.length === 1) {
15350 // Special case size of one
15351 return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) {
15352 newValues[0] = value;
15353 oldValues[0] = oldValue;
15354 listener(newValues, (value === oldValue) ? newValues : oldValues, scope);
15355 });
15356 }
15357
15358 forEach(watchExpressions, function(expr, i) {
15359 var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) {
15360 newValues[i] = value;
15361 oldValues[i] = oldValue;
15362 if (!changeReactionScheduled) {
15363 changeReactionScheduled = true;
15364 self.$evalAsync(watchGroupAction);
15365 }
15366 });
15367 deregisterFns.push(unwatchFn);
15368 });
15369
15370 function watchGroupAction() {
15371 changeReactionScheduled = false;
15372
15373 if (firstRun) {
15374 firstRun = false;
15375 listener(newValues, newValues, self);
15376 } else {
15377 listener(newValues, oldValues, self);
15378 }
15379 }
15380
15381 return function deregisterWatchGroup() {
15382 while (deregisterFns.length) {
15383 deregisterFns.shift()();
15384 }
1222915385 };
1223015386 },
1223115387
1228615442 * de-registration function is executed, the internal watch operation is terminated.
1228715443 */
1228815444 $watchCollection: function(obj, listener) {
15445 $watchCollectionInterceptor.$stateful = true;
15446
1228915447 var self = this;
1229015448 // the current value, updated on each dirty-check run
1229115449 var newValue;
1229715455 // only track veryOldValue if the listener is asking for it
1229815456 var trackVeryOldValue = (listener.length > 1);
1229915457 var changeDetected = 0;
12300 var objGetter = $parse(obj);
15458 var changeDetector = $parse(obj, $watchCollectionInterceptor);
1230115459 var internalArray = [];
1230215460 var internalObject = {};
1230315461 var initRun = true;
1230415462 var oldLength = 0;
1230515463
12306 function $watchCollectionWatch() {
12307 newValue = objGetter(self);
12308 var newLength, key, bothNaN;
15464 function $watchCollectionInterceptor(_value) {
15465 newValue = _value;
15466 var newLength, key, bothNaN, newItem, oldItem;
15467
15468 // If the new value is undefined, then return undefined as the watch may be a one-time watch
15469 if (isUndefined(newValue)) return;
1230915470
1231015471 if (!isObject(newValue)) { // if primitive
1231115472 if (oldValue !== newValue) {
1232915490 }
1233015491 // copy the items to oldValue and look for changes.
1233115492 for (var i = 0; i < newLength; i++) {
12332 bothNaN = (oldValue[i] !== oldValue[i]) &&
12333 (newValue[i] !== newValue[i]);
12334 if (!bothNaN && (oldValue[i] !== newValue[i])) {
15493 oldItem = oldValue[i];
15494 newItem = newValue[i];
15495
15496 bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
15497 if (!bothNaN && (oldItem !== newItem)) {
1233515498 changeDetected++;
12336 oldValue[i] = newValue[i];
15499 oldValue[i] = newItem;
1233715500 }
1233815501 }
1233915502 } else {
1234815511 for (key in newValue) {
1234915512 if (newValue.hasOwnProperty(key)) {
1235015513 newLength++;
12351 if (oldValue.hasOwnProperty(key)) {
12352 bothNaN = (oldValue[key] !== oldValue[key]) &&
12353 (newValue[key] !== newValue[key]);
12354 if (!bothNaN && (oldValue[key] !== newValue[key])) {
15514 newItem = newValue[key];
15515 oldItem = oldValue[key];
15516
15517 if (key in oldValue) {
15518 bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
15519 if (!bothNaN && (oldItem !== newItem)) {
1235515520 changeDetected++;
12356 oldValue[key] = newValue[key];
15521 oldValue[key] = newItem;
1235715522 }
1235815523 } else {
1235915524 oldLength++;
12360 oldValue[key] = newValue[key];
15525 oldValue[key] = newItem;
1236115526 changeDetected++;
1236215527 }
1236315528 }
1236515530 if (oldLength > newLength) {
1236615531 // we used to have more keys, need to find them and destroy them.
1236715532 changeDetected++;
12368 for(key in oldValue) {
12369 if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) {
15533 for (key in oldValue) {
15534 if (!newValue.hasOwnProperty(key)) {
1237015535 oldLength--;
1237115536 delete oldValue[key];
1237215537 }
1240515570 }
1240615571 }
1240715572
12408 return this.$watch($watchCollectionWatch, $watchCollectionAction);
15573 return this.$watch(changeDetector, $watchCollectionAction);
1240915574 },
1241015575
1241115576 /**
1242515590 * {@link ng.directive:ngController controllers} or in
1242615591 * {@link ng.$compileProvider#directive directives}.
1242715592 * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within
12428 * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`.
15593 * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`.
1242915594 *
1243015595 * If you want to be notified whenever `$digest()` is called,
1243115596 * you can register a `watchExpression` function with
1246215627 $digest: function() {
1246315628 var watch, value, last,
1246415629 watchers,
12465 asyncQueue = this.$$asyncQueue,
12466 postDigestQueue = this.$$postDigestQueue,
1246715630 length,
1246815631 dirty, ttl = TTL,
1246915632 next, current, target = this,
1247115634 logIdx, logMsg, asyncTask;
1247215635
1247315636 beginPhase('$digest');
15637 // Check for changes to browser url that happened in sync before the call to $digest
15638 $browser.$$checkUrlChange();
15639
15640 if (this === $rootScope && applyAsyncId !== null) {
15641 // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
15642 // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
15643 $browser.defer.cancel(applyAsyncId);
15644 flushApplyAsync();
15645 }
1247415646
1247515647 lastDirtyWatch = null;
1247615648
1247815650 dirty = false;
1247915651 current = target;
1248015652
12481 while(asyncQueue.length) {
15653 while (asyncQueue.length) {
1248215654 try {
1248315655 asyncTask = asyncQueue.shift();
12484 asyncTask.scope.$eval(asyncTask.expression);
15656 asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
1248515657 } catch (e) {
12486 clearPhase();
1248715658 $exceptionHandler(e);
1248815659 }
1248915660 lastDirtyWatch = null;
1251215683 if (ttl < 5) {
1251315684 logIdx = 4 - ttl;
1251415685 if (!watchLog[logIdx]) watchLog[logIdx] = [];
12515 logMsg = (isFunction(watch.exp))
12516 ? 'fn: ' + (watch.exp.name || watch.exp.toString())
12517 : watch.exp;
12518 logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
12519 watchLog[logIdx].push(logMsg);
15686 watchLog[logIdx].push({
15687 msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
15688 newVal: value,
15689 oldVal: last
15690 });
1252015691 }
1252115692 } else if (watch === lastDirtyWatch) {
1252215693 // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
1252615697 }
1252715698 }
1252815699 } catch (e) {
12529 clearPhase();
1253015700 $exceptionHandler(e);
1253115701 }
1253215702 }
1253515705 // Insanity Warning: scope depth-first traversal
1253615706 // yes, this code is a bit crazy, but it works and we have tests to prove it!
1253715707 // this piece should be kept in sync with the traversal in $broadcast
12538 if (!(next = (current.$$childHead ||
15708 if (!(next = ((current.$$watchersCount && current.$$childHead) ||
1253915709 (current !== target && current.$$nextSibling)))) {
12540 while(current !== target && !(next = current.$$nextSibling)) {
15710 while (current !== target && !(next = current.$$nextSibling)) {
1254115711 current = current.$parent;
1254215712 }
1254315713 }
1254515715
1254615716 // `break traverseScopesLoop;` takes us to here
1254715717
12548 if((dirty || asyncQueue.length) && !(ttl--)) {
15718 if ((dirty || asyncQueue.length) && !(ttl--)) {
1254915719 clearPhase();
1255015720 throw $rootScopeMinErr('infdig',
1255115721 '{0} $digest() iterations reached. Aborting!\n' +
1255215722 'Watchers fired in the last 5 iterations: {1}',
12553 TTL, toJson(watchLog));
15723 TTL, watchLog);
1255415724 }
1255515725
1255615726 } while (dirty || asyncQueue.length);
1255715727
1255815728 clearPhase();
1255915729
12560 while(postDigestQueue.length) {
15730 while (postDigestQueue.length) {
1256115731 try {
1256215732 postDigestQueue.shift()();
1256315733 } catch (e) {
1260215772 * clean up DOM bindings before an element is removed from the DOM.
1260315773 */
1260415774 $destroy: function() {
12605 // we can't destroy the root scope or a scope that has been already destroyed
15775 // We can't destroy a scope that has been already destroyed.
1260615776 if (this.$$destroyed) return;
1260715777 var parent = this.$parent;
1260815778
1260915779 this.$broadcast('$destroy');
1261015780 this.$$destroyed = true;
12611 if (this === $rootScope) return;
12612
12613 forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
15781
15782 if (this === $rootScope) {
15783 //Remove handlers attached to window when $rootScope is removed
15784 $browser.$$applicationDestroyed();
15785 }
15786
15787 incrementWatchersCount(this, -this.$$watchersCount);
15788 for (var eventName in this.$$listenerCount) {
15789 decrementListenerCount(this, this.$$listenerCount[eventName], eventName);
15790 }
1261415791
1261515792 // sever all the references to parent scopes (after this cleanup, the current scope should
1261615793 // not be retained by any of our references and should be eligible for garbage collection)
12617 if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
12618 if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
15794 if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
15795 if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
1261915796 if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
1262015797 if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
1262115798
15799 // Disable listeners, watchers and apply/digest methods
15800 this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
15801 this.$on = this.$watch = this.$watchGroup = function() { return noop; };
15802 this.$$listeners = {};
1262215803
1262315804 // All of the code below is bogus code that works around V8's memory leak via optimized code
1262415805 // and inline caches.
1262915810 // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
1263015811
1263115812 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
12632 this.$$childTail = this.$root = null;
12633
12634 // don't reset these to null in case some async task tries to register a listener/watch/task
12635 this.$$listeners = {};
12636 this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
12637
12638 // prevent NPEs since these methods have references to properties we nulled out
12639 this.$destroy = this.$digest = this.$apply = noop;
12640 this.$on = this.$watch = function() { return noop; };
15813 this.$$childTail = this.$root = this.$$watchers = null;
1264115814 },
1264215815
1264315816 /**
1270015873 * - `string`: execute using the rules as defined in {@link guide/expression expression}.
1270115874 * - `function(scope)`: execute the function with the current `scope` parameter.
1270215875 *
15876 * @param {(object)=} locals Local variables object, useful for overriding values in scope.
1270315877 */
12704 $evalAsync: function(expr) {
15878 $evalAsync: function(expr, locals) {
1270515879 // if we are outside of an $digest loop and this is the first time we are scheduling async
1270615880 // task also schedule async auto-flush
12707 if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
15881 if (!$rootScope.$$phase && !asyncQueue.length) {
1270815882 $browser.defer(function() {
12709 if ($rootScope.$$asyncQueue.length) {
15883 if (asyncQueue.length) {
1271015884 $rootScope.$digest();
1271115885 }
1271215886 });
1271315887 }
1271415888
12715 this.$$asyncQueue.push({scope: this, expression: expr});
15889 asyncQueue.push({scope: this, expression: expr, locals: locals});
1271615890 },
1271715891
12718 $$postDigest : function(fn) {
12719 this.$$postDigestQueue.push(fn);
15892 $$postDigest: function(fn) {
15893 postDigestQueue.push(fn);
1272015894 },
1272115895
1272215896 /**
1278315957
1278415958 /**
1278515959 * @ngdoc method
15960 * @name $rootScope.Scope#$applyAsync
15961 * @kind function
15962 *
15963 * @description
15964 * Schedule the invocation of $apply to occur at a later time. The actual time difference
15965 * varies across browsers, but is typically around ~10 milliseconds.
15966 *
15967 * This can be used to queue up multiple expressions which need to be evaluated in the same
15968 * digest.
15969 *
15970 * @param {(string|function())=} exp An angular expression to be executed.
15971 *
15972 * - `string`: execute using the rules as defined in {@link guide/expression expression}.
15973 * - `function(scope)`: execute the function with current `scope` parameter.
15974 */
15975 $applyAsync: function(expr) {
15976 var scope = this;
15977 expr && applyAsyncQueue.push($applyAsyncExpression);
15978 scheduleApplyAsync();
15979
15980 function $applyAsyncExpression() {
15981 scope.$eval(expr);
15982 }
15983 },
15984
15985 /**
15986 * @ngdoc method
1278615987 * @name $rootScope.Scope#$on
1278715988 * @kind function
1278815989 *
1279515996 *
1279615997 * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or
1279715998 * `$broadcast`-ed.
12798 * - `currentScope` - `{Scope}`: the current scope which is handling the event.
15999 * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the
16000 * event propagates through the scope hierarchy, this property is set to null.
1279916001 * - `name` - `{string}`: name of the event.
1280016002 * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel
1280116003 * further event propagation (available only for events that were `$emit`-ed).
1282416026
1282516027 var self = this;
1282616028 return function() {
12827 namedListeners[indexOf(namedListeners, listener)] = null;
12828 decrementListenerCount(self, 1, name);
16029 var indexOfListener = namedListeners.indexOf(listener);
16030 if (indexOfListener !== -1) {
16031 namedListeners[indexOfListener] = null;
16032 decrementListenerCount(self, 1, name);
16033 }
1282916034 };
1283016035 },
1283116036
1287216077 do {
1287316078 namedListeners = scope.$$listeners[name] || empty;
1287416079 event.currentScope = scope;
12875 for (i=0, length=namedListeners.length; i<length; i++) {
16080 for (i = 0, length = namedListeners.length; i < length; i++) {
1287616081
1287716082 // if listeners were deregistered, defragment the array
1287816083 if (!namedListeners[i]) {
1288916094 }
1289016095 }
1289116096 //if any listener on the current scope stops propagation, prevent bubbling
12892 if (stopPropagation) return event;
16097 if (stopPropagation) {
16098 event.currentScope = null;
16099 return event;
16100 }
1289316101 //traverse upwards
1289416102 scope = scope.$parent;
1289516103 } while (scope);
16104
16105 event.currentScope = null;
1289616106
1289716107 return event;
1289816108 },
1293016140 event.defaultPrevented = true;
1293116141 },
1293216142 defaultPrevented: false
12933 },
12934 listenerArgs = concat([event], arguments, 1),
16143 };
16144
16145 if (!target.$$listenerCount[name]) return event;
16146
16147 var listenerArgs = concat([event], arguments, 1),
1293516148 listeners, i, length;
1293616149
1293716150 //down while you can, then up and next sibling or up and next sibling until back at root
1293816151 while ((current = next)) {
1293916152 event.currentScope = current;
1294016153 listeners = current.$$listeners[name] || [];
12941 for (i=0, length = listeners.length; i<length; i++) {
16154 for (i = 0, length = listeners.length; i < length; i++) {
1294216155 // if listeners were deregistered, defragment the array
1294316156 if (!listeners[i]) {
1294416157 listeners.splice(i, 1);
1294916162
1295016163 try {
1295116164 listeners[i].apply(null, listenerArgs);
12952 } catch(e) {
16165 } catch (e) {
1295316166 $exceptionHandler(e);
1295416167 }
1295516168 }
1296016173 // (though it differs due to having the extra check for $$listenerCount)
1296116174 if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
1296216175 (current !== target && current.$$nextSibling)))) {
12963 while(current !== target && !(next = current.$$nextSibling)) {
16176 while (current !== target && !(next = current.$$nextSibling)) {
1296416177 current = current.$parent;
1296516178 }
1296616179 }
1296716180 }
1296816181
16182 event.currentScope = null;
1296916183 return event;
1297016184 }
1297116185 };
1297216186
1297316187 var $rootScope = new Scope();
16188
16189 //The internal queues. Expose them on the $rootScope for debugging/testing purposes.
16190 var asyncQueue = $rootScope.$$asyncQueue = [];
16191 var postDigestQueue = $rootScope.$$postDigestQueue = [];
16192 var applyAsyncQueue = $rootScope.$$applyAsyncQueue = [];
1297416193
1297516194 return $rootScope;
1297616195
1298716206 $rootScope.$$phase = null;
1298816207 }
1298916208
12990 function compileToFn(exp, name) {
12991 var fn = $parse(exp);
12992 assertArgFn(fn, name);
12993 return fn;
16209 function incrementWatchersCount(current, count) {
16210 do {
16211 current.$$watchersCount += count;
16212 } while ((current = current.$parent));
1299416213 }
1299516214
1299616215 function decrementListenerCount(current, count, name) {
1300816227 * because it's unique we can easily tell it apart from other values
1300916228 */
1301016229 function initWatchVal() {}
16230
16231 function flushApplyAsync() {
16232 while (applyAsyncQueue.length) {
16233 try {
16234 applyAsyncQueue.shift()();
16235 } catch (e) {
16236 $exceptionHandler(e);
16237 }
16238 }
16239 applyAsyncId = null;
16240 }
16241
16242 function scheduleApplyAsync() {
16243 if (applyAsyncId === null) {
16244 applyAsyncId = $browser.defer(function() {
16245 $rootScope.$apply(flushApplyAsync);
16246 });
16247 }
16248 }
1301116249 }];
1301216250 }
1301316251
1301716255 */
1301816256 function $$SanitizeUriProvider() {
1301916257 var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/,
13020 imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file):|data:image\/)/;
16258 imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
1302116259
1302216260 /**
1302316261 * @description
1307216310 return function sanitizeUri(uri, isImage) {
1307316311 var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
1307416312 var normalizedVal;
13075 // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case.
13076 if (!msie || msie >= 8 ) {
13077 normalizedVal = urlResolve(uri).href;
13078 if (normalizedVal !== '' && !normalizedVal.match(regex)) {
13079 return 'unsafe:'+normalizedVal;
13080 }
16313 normalizedVal = urlResolve(uri).href;
16314 if (normalizedVal !== '' && !normalizedVal.match(regex)) {
16315 return 'unsafe:' + normalizedVal;
1308116316 }
1308216317 return uri;
1308316318 };
1308416319 };
1308516320 }
16321
16322 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
16323 * Any commits to this file should be reviewed with security in mind. *
16324 * Changes to this file can potentially create security vulnerabilities. *
16325 * An approval from 2 Core members with history of modifying *
16326 * this file is required. *
16327 * *
16328 * Does the change somehow allow for arbitrary javascript to be executed? *
16329 * Or allows for someone to change the prototype of built-in objects? *
16330 * Or gives undesired access to variables likes document or window? *
16331 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
1308616332
1308716333 var $sceMinErr = minErr('$sce');
1308816334
1309716343 };
1309816344
1309916345 // Helper functions follow.
13100
13101 // Copied from:
13102 // http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962
13103 // Prereq: s is a string.
13104 function escapeForRegexp(s) {
13105 return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
13106 replace(/\x08/g, '\\x08');
13107 }
13108
1310916346
1311016347 function adjustMatcher(matcher) {
1311116348 if (matcher === 'self') {
1324216479 * @description
1324316480 * Sets/Gets the whitelist of trusted resource URLs.
1324416481 */
13245 this.resourceUrlWhitelist = function (value) {
16482 this.resourceUrlWhitelist = function(value) {
1324616483 if (arguments.length) {
1324716484 resourceUrlWhitelist = adjustMatchers(value);
1324816485 }
1327616513 * Sets/Gets the blacklist of trusted resource URLs.
1327716514 */
1327816515
13279 this.resourceUrlBlacklist = function (value) {
16516 this.resourceUrlBlacklist = function(value) {
1328016517 if (arguments.length) {
1328116518 resourceUrlBlacklist = adjustMatchers(value);
1328216519 }
1349416731 *
1349516732 * As of version 1.2, Angular ships with SCE enabled by default.
1349616733 *
13497 * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows
16734 * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow
1349816735 * one to execute arbitrary javascript by the use of the expression() syntax. Refer
1349916736 * <http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx> to learn more about them.
1350016737 * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>`
1350616743 * Here's an example of a binding in a privileged context:
1350716744 *
1350816745 * ```
13509 * <input ng-model="userHtml">
16746 * <input ng-model="userHtml" aria-label="User input">
1351016747 * <div ng-bind-html="userHtml"></div>
1351116748 * ```
1351216749 *
1354116778 *
1354216779 * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted
1354316780 * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link
13544 * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
16781 * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
1354516782 * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals.
1354616783 *
1354716784 * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
1357816815 * won't work on all browsers. Also, loading templates from `file://` URL does not work on some
1357916816 * browsers.
1358016817 *
13581 * ## This feels like too much overhead for the developer?
16818 * ## This feels like too much overhead
1358216819 *
1358316820 * It's important to remember that SCE only applies to interpolation expressions.
1358416821 *
1366216899 *
1366316900 * <example module="mySceApp" deps="angular-sanitize.js">
1366416901 * <file name="index.html">
13665 * <div ng-controller="myAppController as myCtrl">
16902 * <div ng-controller="AppController as myCtrl">
1366616903 * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br>
1366716904 * <b>User comments</b><br>
1366816905 * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when
1367916916 * </file>
1368016917 *
1368116918 * <file name="script.js">
13682 * var mySceApp = angular.module('mySceApp', ['ngSanitize']);
13683 *
13684 * mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) {
13685 * var self = this;
13686 * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
13687 * self.userComments = userComments;
13688 * });
13689 * self.explicitlyTrustedHtml = $sce.trustAsHtml(
13690 * '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
13691 * 'sanitization.&quot;">Hover over this text.</span>');
13692 * });
16919 * angular.module('mySceApp', ['ngSanitize'])
16920 * .controller('AppController', ['$http', '$templateCache', '$sce',
16921 * function($http, $templateCache, $sce) {
16922 * var self = this;
16923 * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
16924 * self.userComments = userComments;
16925 * });
16926 * self.explicitlyTrustedHtml = $sce.trustAsHtml(
16927 * '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
16928 * 'sanitization.&quot;">Hover over this text.</span>');
16929 * }]);
1369316930 * </file>
1369416931 *
1369516932 * <file name="test_data.json">
1375716994 * @description
1375816995 * Enables/disables SCE and returns the current value.
1375916996 */
13760 this.enabled = function (value) {
16997 this.enabled = function(value) {
1376116998 if (arguments.length) {
1376216999 enabled = !!value;
1376317000 }
1381117048 * sce.js and sceSpecs.js would need to be aware of this detail.
1381217049 */
1381317050
13814 this.$get = ['$parse', '$sniffer', '$sceDelegate', function(
13815 $parse, $sniffer, $sceDelegate) {
13816 // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows
17051 this.$get = ['$parse', '$sceDelegate', function(
17052 $parse, $sceDelegate) {
17053 // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow
1381717054 // the "expression(javascript expression)" syntax which is insecure.
13818 if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) {
17055 if (enabled && msie < 8) {
1381917056 throw $sceMinErr('iequirks',
13820 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' +
17057 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' +
1382117058 'mode. You can fix this by adding the text <!doctype html> to the top of your HTML ' +
1382217059 'document. See http://docs.angularjs.org/api/ng.$sce for more information.');
1382317060 }
1383517072 * @description
1383617073 * Returns a boolean indicating if SCE is enabled.
1383717074 */
13838 sce.isEnabled = function () {
17075 sce.isEnabled = function() {
1383917076 return enabled;
1384017077 };
1384117078 sce.trustAs = $sceDelegate.trustAs;
1387117108 if (parsed.literal && parsed.constant) {
1387217109 return parsed;
1387317110 } else {
13874 return function sceParseAsTrusted(self, locals) {
13875 return sce.getTrusted(type, parsed(self, locals));
13876 };
17111 return $parse(expr, function(value) {
17112 return sce.getTrusted(type, value);
17113 });
1387717114 }
1387817115 };
1387917116
1389017127 * escaping.
1389117128 *
1389217129 * @param {string} type The kind of context in which this value is safe for use. e.g. url,
13893 * resource_url, html, js and css.
17130 * resourceUrl, html, js and css.
1389417131 * @param {*} value The value that that should be considered trusted/safe.
1389517132 * @returns {*} A value that can be used to stand in for the provided `value` in places
1389617133 * where Angular expects a $sce.trustAs() return value.
1404017277 *
1404117278 * @description
1404217279 * Shorthand method. `$sce.parseAsHtml(expression string)` →
14043 * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`}
17280 * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`}
1404417281 *
1404517282 * @param {string} expression String expression to compile.
1404617283 * @returns {function(context, locals)} a function which represents the compiled expression:
1405717294 *
1405817295 * @description
1405917296 * Shorthand method. `$sce.parseAsCss(value)` →
14060 * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`}
17297 * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`}
1406117298 *
1406217299 * @param {string} expression String expression to compile.
1406317300 * @returns {function(context, locals)} a function which represents the compiled expression:
1407417311 *
1407517312 * @description
1407617313 * Shorthand method. `$sce.parseAsUrl(value)` →
14077 * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`}
17314 * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`}
1407817315 *
1407917316 * @param {string} expression String expression to compile.
1408017317 * @returns {function(context, locals)} a function which represents the compiled expression:
1409117328 *
1409217329 * @description
1409317330 * Shorthand method. `$sce.parseAsResourceUrl(value)` →
14094 * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`}
17331 * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`}
1409517332 *
1409617333 * @param {string} expression String expression to compile.
1409717334 * @returns {function(context, locals)} a function which represents the compiled expression:
1410817345 *
1410917346 * @description
1411017347 * Shorthand method. `$sce.parseAsJs(value)` →
14111 * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`}
17348 * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`}
1411217349 *
1411317350 * @param {string} expression String expression to compile.
1411417351 * @returns {function(context, locals)} a function which represents the compiled expression:
1412417361 getTrusted = sce.getTrusted,
1412517362 trustAs = sce.trustAs;
1412617363
14127 forEach(SCE_CONTEXTS, function (enumValue, name) {
17364 forEach(SCE_CONTEXTS, function(enumValue, name) {
1412817365 var lName = lowercase(name);
14129 sce[camelCase("parse_as_" + lName)] = function (expr) {
17366 sce[camelCase("parse_as_" + lName)] = function(expr) {
1413017367 return parse(enumValue, expr);
1413117368 };
14132 sce[camelCase("get_trusted_" + lName)] = function (value) {
17369 sce[camelCase("get_trusted_" + lName)] = function(value) {
1413317370 return getTrusted(enumValue, value);
1413417371 };
14135 sce[camelCase("trust_as_" + lName)] = function (value) {
17372 sce[camelCase("trust_as_" + lName)] = function(value) {
1413617373 return trustAs(enumValue, value);
1413717374 };
1413817375 });
1414917386 * @requires $document
1415017387 *
1415117388 * @property {boolean} history Does the browser support html5 history api ?
14152 * @property {boolean} hashchange Does the browser support hashchange event ?
1415317389 * @property {boolean} transitions Does the browser support CSS transition events ?
1415417390 * @property {boolean} animations Does the browser support CSS animation events ?
1415517391 *
1416017396 this.$get = ['$window', '$document', function($window, $document) {
1416117397 var eventSupport = {},
1416217398 android =
14163 int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
17399 toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
1416417400 boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
1416517401 document = $document[0] || {},
14166 documentMode = document.documentMode,
1416717402 vendorPrefix,
14168 vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/,
17403 vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/,
1416917404 bodyStyle = document.body && document.body.style,
1417017405 transitions = false,
1417117406 animations = false,
1417217407 match;
1417317408
1417417409 if (bodyStyle) {
14175 for(var prop in bodyStyle) {
14176 if(match = vendorRegex.exec(prop)) {
17410 for (var prop in bodyStyle) {
17411 if (match = vendorRegex.exec(prop)) {
1417717412 vendorPrefix = match[0];
1417817413 vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1);
1417917414 break;
1418017415 }
1418117416 }
1418217417
14183 if(!vendorPrefix) {
17418 if (!vendorPrefix) {
1418417419 vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit';
1418517420 }
1418617421
1418717422 transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle));
1418817423 animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
1418917424
14190 if (android && (!transitions||!animations)) {
14191 transitions = isString(document.body.style.webkitTransition);
14192 animations = isString(document.body.style.webkitAnimation);
17425 if (android && (!transitions || !animations)) {
17426 transitions = isString(bodyStyle.webkitTransition);
17427 animations = isString(bodyStyle.webkitAnimation);
1419317428 }
1419417429 }
1419517430
1420617441 // jshint -W018
1420717442 history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
1420817443 // jshint +W018
14209 hashchange: 'onhashchange' in $window &&
14210 // IE8 compatible mode lies
14211 (!documentMode || documentMode > 7),
1421217444 hasEvent: function(event) {
1421317445 // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
1421417446 // it. In particular the event is not fired when backspace or delete key are pressed or
1421517447 // when cut operation is performed.
14216 if (event == 'input' && msie == 9) return false;
17448 // IE10+ implements 'input' event but it erroneously fires under various situations,
17449 // e.g. when placeholder changes, or a form is focused.
17450 if (event === 'input' && msie <= 11) return false;
1421717451
1421817452 if (isUndefined(eventSupport[event])) {
1421917453 var divElm = document.createElement('div');
1422417458 },
1422517459 csp: csp(),
1422617460 vendorPrefix: vendorPrefix,
14227 transitions : transitions,
14228 animations : animations,
14229 android: android,
14230 msie : msie,
14231 msieDocumentMode: documentMode
17461 transitions: transitions,
17462 animations: animations,
17463 android: android
1423217464 };
1423317465 }];
1423417466 }
1423517467
17468 var $compileMinErr = minErr('$compile');
17469
17470 /**
17471 * @ngdoc service
17472 * @name $templateRequest
17473 *
17474 * @description
17475 * The `$templateRequest` service runs security checks then downloads the provided template using
17476 * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request
17477 * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the
17478 * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the
17479 * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted
17480 * when `tpl` is of type string and `$templateCache` has the matching entry.
17481 *
17482 * @param {string|TrustedResourceUrl} tpl The HTTP request template URL
17483 * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
17484 *
17485 * @return {Promise} a promise for the HTTP response data of the given URL.
17486 *
17487 * @property {number} totalPendingRequests total amount of pending template requests being downloaded.
17488 */
17489 function $TemplateRequestProvider() {
17490 this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) {
17491 function handleRequestFn(tpl, ignoreRequestError) {
17492 handleRequestFn.totalPendingRequests++;
17493
17494 // We consider the template cache holds only trusted templates, so
17495 // there's no need to go through whitelisting again for keys that already
17496 // are included in there. This also makes Angular accept any script
17497 // directive, no matter its name. However, we still need to unwrap trusted
17498 // types.
17499 if (!isString(tpl) || !$templateCache.get(tpl)) {
17500 tpl = $sce.getTrustedResourceUrl(tpl);
17501 }
17502
17503 var transformResponse = $http.defaults && $http.defaults.transformResponse;
17504
17505 if (isArray(transformResponse)) {
17506 transformResponse = transformResponse.filter(function(transformer) {
17507 return transformer !== defaultHttpResponseTransform;
17508 });
17509 } else if (transformResponse === defaultHttpResponseTransform) {
17510 transformResponse = null;
17511 }
17512
17513 var httpOptions = {
17514 cache: $templateCache,
17515 transformResponse: transformResponse
17516 };
17517
17518 return $http.get(tpl, httpOptions)
17519 ['finally'](function() {
17520 handleRequestFn.totalPendingRequests--;
17521 })
17522 .then(function(response) {
17523 $templateCache.put(tpl, response.data);
17524 return response.data;
17525 }, handleError);
17526
17527 function handleError(resp) {
17528 if (!ignoreRequestError) {
17529 throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
17530 tpl, resp.status, resp.statusText);
17531 }
17532 return $q.reject(resp);
17533 }
17534 }
17535
17536 handleRequestFn.totalPendingRequests = 0;
17537
17538 return handleRequestFn;
17539 }];
17540 }
17541
17542 function $$TestabilityProvider() {
17543 this.$get = ['$rootScope', '$browser', '$location',
17544 function($rootScope, $browser, $location) {
17545
17546 /**
17547 * @name $testability
17548 *
17549 * @description
17550 * The private $$testability service provides a collection of methods for use when debugging
17551 * or by automated test and debugging tools.
17552 */
17553 var testability = {};
17554
17555 /**
17556 * @name $$testability#findBindings
17557 *
17558 * @description
17559 * Returns an array of elements that are bound (via ng-bind or {{}})
17560 * to expressions matching the input.
17561 *
17562 * @param {Element} element The element root to search from.
17563 * @param {string} expression The binding expression to match.
17564 * @param {boolean} opt_exactMatch If true, only returns exact matches
17565 * for the expression. Filters and whitespace are ignored.
17566 */
17567 testability.findBindings = function(element, expression, opt_exactMatch) {
17568 var bindings = element.getElementsByClassName('ng-binding');
17569 var matches = [];
17570 forEach(bindings, function(binding) {
17571 var dataBinding = angular.element(binding).data('$binding');
17572 if (dataBinding) {
17573 forEach(dataBinding, function(bindingName) {
17574 if (opt_exactMatch) {
17575 var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)');
17576 if (matcher.test(bindingName)) {
17577 matches.push(binding);
17578 }
17579 } else {
17580 if (bindingName.indexOf(expression) != -1) {
17581 matches.push(binding);
17582 }
17583 }
17584 });
17585 }
17586 });
17587 return matches;
17588 };
17589
17590 /**
17591 * @name $$testability#findModels
17592 *
17593 * @description
17594 * Returns an array of elements that are two-way found via ng-model to
17595 * expressions matching the input.
17596 *
17597 * @param {Element} element The element root to search from.
17598 * @param {string} expression The model expression to match.
17599 * @param {boolean} opt_exactMatch If true, only returns exact matches
17600 * for the expression.
17601 */
17602 testability.findModels = function(element, expression, opt_exactMatch) {
17603 var prefixes = ['ng-', 'data-ng-', 'ng\\:'];
17604 for (var p = 0; p < prefixes.length; ++p) {
17605 var attributeEquals = opt_exactMatch ? '=' : '*=';
17606 var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
17607 var elements = element.querySelectorAll(selector);
17608 if (elements.length) {
17609 return elements;
17610 }
17611 }
17612 };
17613
17614 /**
17615 * @name $$testability#getLocation
17616 *
17617 * @description
17618 * Shortcut for getting the location in a browser agnostic way. Returns
17619 * the path, search, and hash. (e.g. /path?a=b#hash)
17620 */
17621 testability.getLocation = function() {
17622 return $location.url();
17623 };
17624
17625 /**
17626 * @name $$testability#setLocation
17627 *
17628 * @description
17629 * Shortcut for navigating to a location without doing a full page reload.
17630 *
17631 * @param {string} url The location url (path, search and hash,
17632 * e.g. /path?a=b#hash) to go to.
17633 */
17634 testability.setLocation = function(url) {
17635 if (url !== $location.url()) {
17636 $location.url(url);
17637 $rootScope.$digest();
17638 }
17639 };
17640
17641 /**
17642 * @name $$testability#whenStable
17643 *
17644 * @description
17645 * Calls the callback when $timeout and $http requests are completed.
17646 *
17647 * @param {function} callback
17648 */
17649 testability.whenStable = function(callback) {
17650 $browser.notifyWhenNoOutstandingRequests(callback);
17651 };
17652
17653 return testability;
17654 }];
17655 }
17656
1423617657 function $TimeoutProvider() {
14237 this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler',
14238 function($rootScope, $browser, $q, $exceptionHandler) {
17658 this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
17659 function($rootScope, $browser, $q, $$q, $exceptionHandler) {
17660
1423917661 var deferreds = {};
1424017662
1424117663
1424817670 * block and delegates any exceptions to
1424917671 * {@link ng.$exceptionHandler $exceptionHandler} service.
1425017672 *
14251 * The return value of registering a timeout function is a promise, which will be resolved when
14252 * the timeout is reached and the timeout function is executed.
17673 * The return value of calling `$timeout` is a promise, which will be resolved when
17674 * the delay has passed and the timeout function, if provided, is executed.
1425317675 *
1425417676 * To cancel a timeout request, call `$timeout.cancel(promise)`.
1425517677 *
1425617678 * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
1425717679 * synchronously flush the queue of deferred functions.
1425817680 *
14259 * @param {function()} fn A function, whose execution should be delayed.
17681 * If you only want a promise that will be resolved after some specified delay
17682 * then you can call `$timeout` without the `fn` function.
17683 *
17684 * @param {function()=} fn A function, whose execution should be delayed.
1426017685 * @param {number=} [delay=0] Delay in milliseconds.
1426117686 * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
1426217687 * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
17688 * @param {...*=} Pass additional parameters to the executed function.
1426317689 * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
1426417690 * promise will be resolved with is the return value of the `fn` function.
1426517691 *
1426617692 */
1426717693 function timeout(fn, delay, invokeApply) {
14268 var deferred = $q.defer(),
17694 if (!isFunction(fn)) {
17695 invokeApply = delay;
17696 delay = fn;
17697 fn = noop;
17698 }
17699
17700 var args = sliceArgs(arguments, 3),
17701 skipApply = (isDefined(invokeApply) && !invokeApply),
17702 deferred = (skipApply ? $$q : $q).defer(),
1426917703 promise = deferred.promise,
14270 skipApply = (isDefined(invokeApply) && !invokeApply),
1427117704 timeoutId;
1427217705
1427317706 timeoutId = $browser.defer(function() {
1427417707 try {
14275 deferred.resolve(fn());
14276 } catch(e) {
17708 deferred.resolve(fn.apply(null, args));
17709 } catch (e) {
1427717710 deferred.reject(e);
1427817711 $exceptionHandler(e);
1427917712 }
1432417757 // exactly the behavior needed here. There is little value is mocking these out for this
1432517758 // service.
1432617759 var urlParsingNode = document.createElement("a");
14327 var originUrl = urlResolve(window.location.href, true);
17760 var originUrl = urlResolve(window.location.href);
1432817761
1432917762
1433017763 /**
1437917812 * | pathname | The pathname, beginning with "/"
1438017813 *
1438117814 */
14382 function urlResolve(url, base) {
17815 function urlResolve(url) {
1438317816 var href = url;
1438417817
1438517818 if (msie) {
1443917872 <file name="index.html">
1444017873 <script>
1444117874 angular.module('windowExample', [])
14442 .controller('ExampleController', ['$scope', '$window', function ($scope, $window) {
17875 .controller('ExampleController', ['$scope', '$window', function($scope, $window) {
1444317876 $scope.greeting = 'Hello, World!';
1444417877 $scope.doGreeting = function(greeting) {
1444517878 $window.alert(greeting);
1444717880 }]);
1444817881 </script>
1444917882 <div ng-controller="ExampleController">
14450 <input type="text" ng-model="greeting" />
17883 <input type="text" ng-model="greeting" aria-label="greeting" />
1445117884 <button ng-click="doGreeting(greeting)">ALERT</button>
1445217885 </div>
1445317886 </file>
1446017893 </file>
1446117894 </example>
1446217895 */
14463 function $WindowProvider(){
17896 function $WindowProvider() {
1446417897 this.$get = valueFn(window);
17898 }
17899
17900 /**
17901 * @name $$cookieReader
17902 * @requires $document
17903 *
17904 * @description
17905 * This is a private service for reading cookies used by $http and ngCookies
17906 *
17907 * @return {Object} a key/value map of the current cookies
17908 */
17909 function $$CookieReader($document) {
17910 var rawDocument = $document[0] || {};
17911 var lastCookies = {};
17912 var lastCookieString = '';
17913
17914 function safeDecodeURIComponent(str) {
17915 try {
17916 return decodeURIComponent(str);
17917 } catch (e) {
17918 return str;
17919 }
17920 }
17921
17922 return function() {
17923 var cookieArray, cookie, i, index, name;
17924 var currentCookieString = rawDocument.cookie || '';
17925
17926 if (currentCookieString !== lastCookieString) {
17927 lastCookieString = currentCookieString;
17928 cookieArray = lastCookieString.split('; ');
17929 lastCookies = {};
17930
17931 for (i = 0; i < cookieArray.length; i++) {
17932 cookie = cookieArray[i];
17933 index = cookie.indexOf('=');
17934 if (index > 0) { //ignore nameless cookies
17935 name = safeDecodeURIComponent(cookie.substring(0, index));
17936 // the first value that is seen for a cookie is the most
17937 // specific one. values for the same cookie name that
17938 // follow are for less specific paths.
17939 if (lastCookies[name] === undefined) {
17940 lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1));
17941 }
17942 }
17943 }
17944 }
17945 return lastCookies;
17946 };
17947 }
17948
17949 $$CookieReader.$inject = ['$document'];
17950
17951 function $$CookieReaderProvider() {
17952 this.$get = $$CookieReader;
1446517953 }
1446617954
1446717955 /* global currencyFilter: true,
1448317971 * Filters are just functions which transform input to an output. However filters need to be
1448417972 * Dependency Injected. To achieve this a filter definition consists of a factory function which is
1448517973 * annotated with dependencies and is responsible for creating a filter function.
17974 *
17975 * <div class="alert alert-warning">
17976 * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
17977 * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
17978 * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
17979 * (`myapp_subsection_filterx`).
17980 * </div>
1448617981 *
1448717982 * ```js
1448817983 * // Filter registration
1452418019 * For more information about how angular filters work, and how to create your own filters, see
1452518020 * {@link guide/filter Filters} in the Angular Developer Guide.
1452618021 */
14527 /**
14528 * @ngdoc method
14529 * @name $filterProvider#register
14530 * @description
14531 * Register filter factory function.
14532 *
14533 * @param {String} name Name of the filter.
14534 * @param {Function} fn The filter factory function which is injectable.
14535 */
14536
1453718022
1453818023 /**
1453918024 * @ngdoc service
1457218057
1457318058 /**
1457418059 * @ngdoc method
14575 * @name $controllerProvider#register
18060 * @name $filterProvider#register
1457618061 * @param {string|Object} name Name of the filter function, or an object map of filters where
1457718062 * the keys are the filter names and the values are the filter factories.
18063 *
18064 * <div class="alert alert-warning">
18065 * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
18066 * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
18067 * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
18068 * (`myapp_subsection_filterx`).
18069 * </div>
1457818070 * @returns {Object} Registered filter instance, or if a map of filters was provided then a map
1457918071 * of the registered filter instances.
1458018072 */
1458118073 function register(name, factory) {
14582 if(isObject(name)) {
18074 if (isObject(name)) {
1458318075 var filters = {};
1458418076 forEach(name, function(filter, key) {
1458518077 filters[key] = register(key, filter);
1463618128 *
1463718129 * Can be one of:
1463818130 *
14639 * - `string`: The string is evaluated as an expression and the resulting value is used for substring match against
14640 * the contents of the `array`. All strings or objects with string properties in `array` that contain this string
14641 * will be returned. The predicate can be negated by prefixing the string with `!`.
18131 * - `string`: The string is used for matching against the contents of the `array`. All strings or
18132 * objects with string properties in `array` that match this string will be returned. This also
18133 * applies to nested object properties.
18134 * The predicate can be negated by prefixing the string with `!`.
1464218135 *
1464318136 * - `Object`: A pattern object can be used to filter specific properties on objects contained
1464418137 * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
1464518138 * which have property `name` containing "M" and property `phone` containing "1". A special
1464618139 * property name `$` can be used (as in `{$:"text"}`) to accept a match against any
14647 * property of the object. That's equivalent to the simple substring match with a `string`
14648 * as described above.
14649 *
14650 * - `function(value)`: A predicate function can be used to write arbitrary filters. The function is
14651 * called for each element of `array`. The final result is an array of those elements that
14652 * the predicate returned true for.
18140 * property of the object or its nested object properties. That's equivalent to the simple
18141 * substring match with a `string` as described above. The predicate can be negated by prefixing
18142 * the string with `!`.
18143 * For example `{name: "!M"}` predicate will return an array of items which have property `name`
18144 * not containing "M".
18145 *
18146 * Note that a named property will match properties on the same level only, while the special
18147 * `$` property will match properties on the same level or deeper. E.g. an array item like
18148 * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but
18149 * **will** be matched by `{$: 'John'}`.
18150 *
18151 * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters.
18152 * The function is called for each element of the array, with the element, its index, and
18153 * the entire array itself as arguments.
18154 *
18155 * The final result is an array of those elements that the predicate returned true for.
1465318156 *
1465418157 * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in
1465518158 * determining if the expected value (from the filter expression) and actual value (from
1465918162 *
1466018163 * - `function(actual, expected)`:
1466118164 * The function will be given the object value and the predicate value to compare and
14662 * should return true if the item should be included in filtered result.
14663 *
14664 * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`.
14665 * this is essentially strict comparison of expected and actual.
18165 * should return true if both values should be considered equal.
18166 *
18167 * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`.
18168 * This is essentially strict comparison of expected and actual.
1466618169 *
1466718170 * - `false|undefined`: A short hand for a function which will look for a substring match in case
1466818171 * insensitive way.
18172 *
18173 * Primitive values are converted to strings. Objects are not compared against primitives,
18174 * unless they have a custom `toString` method (e.g. `Date` objects).
1466918175 *
1467018176 * @example
1467118177 <example>
1467718183 {name:'Julie', phone:'555-8765'},
1467818184 {name:'Juliette', phone:'555-5678'}]"></div>
1467918185
14680 Search: <input ng-model="searchText">
18186 <label>Search: <input ng-model="searchText"></label>
1468118187 <table id="searchTextResults">
1468218188 <tr><th>Name</th><th>Phone</th></tr>
1468318189 <tr ng-repeat="friend in friends | filter:searchText">
1468618192 </tr>
1468718193 </table>
1468818194 <hr>
14689 Any: <input ng-model="search.$"> <br>
14690 Name only <input ng-model="search.name"><br>
14691 Phone only <input ng-model="search.phone"><br>
14692 Equality <input type="checkbox" ng-model="strict"><br>
18195 <label>Any: <input ng-model="search.$"></label> <br>
18196 <label>Name only <input ng-model="search.name"></label><br>
18197 <label>Phone only <input ng-model="search.phone"></label><br>
18198 <label>Equality <input type="checkbox" ng-model="strict"></label><br>
1469318199 <table id="searchObjResults">
1469418200 <tr><th>Name</th><th>Phone</th></tr>
1469518201 <tr ng-repeat="friendObj in friends | filter:search:strict">
1473718243 */
1473818244 function filterFilter() {
1473918245 return function(array, expression, comparator) {
14740 if (!isArray(array)) return array;
14741
14742 var comparatorType = typeof(comparator),
14743 predicates = [];
14744
14745 predicates.check = function(value) {
14746 for (var j = 0; j < predicates.length; j++) {
14747 if(!predicates[j](value)) {
14748 return false;
14749 }
14750 }
14751 return true;
14752 };
14753
14754 if (comparatorType !== 'function') {
14755 if (comparatorType === 'boolean' && comparator) {
14756 comparator = function(obj, text) {
14757 return angular.equals(obj, text);
14758 };
18246 if (!isArrayLike(array)) {
18247 if (array == null) {
18248 return array;
1475918249 } else {
14760 comparator = function(obj, text) {
14761 if (obj && text && typeof obj === 'object' && typeof text === 'object') {
14762 for (var objKey in obj) {
14763 if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) &&
14764 comparator(obj[objKey], text[objKey])) {
14765 return true;
14766 }
14767 }
14768 return false;
14769 }
14770 text = (''+text).toLowerCase();
14771 return (''+obj).toLowerCase().indexOf(text) > -1;
14772 };
14773 }
14774 }
14775
14776 var search = function(obj, text){
14777 if (typeof text == 'string' && text.charAt(0) === '!') {
14778 return !search(obj, text.substr(1));
14779 }
14780 switch (typeof obj) {
14781 case "boolean":
14782 case "number":
14783 case "string":
14784 return comparator(obj, text);
14785 case "object":
14786 switch (typeof text) {
14787 case "object":
14788 return comparator(obj, text);
14789 default:
14790 for ( var objKey in obj) {
14791 if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
14792 return true;
14793 }
14794 }
14795 break;
14796 }
14797 return false;
14798 case "array":
14799 for ( var i = 0; i < obj.length; i++) {
14800 if (search(obj[i], text)) {
14801 return true;
14802 }
14803 }
14804 return false;
14805 default:
14806 return false;
14807 }
14808 };
14809 switch (typeof expression) {
14810 case "boolean":
14811 case "number":
14812 case "string":
14813 // Set up expression object and fall through
14814 expression = {$:expression};
14815 // jshint -W086
14816 case "object":
14817 // jshint +W086
14818 for (var key in expression) {
14819 (function(path) {
14820 if (typeof expression[path] === 'undefined') return;
14821 predicates.push(function(value) {
14822 return search(path == '$' ? value : (value && value[path]), expression[path]);
14823 });
14824 })(key);
14825 }
18250 throw minErr('filter')('notarray', 'Expected array but received: {0}', array);
18251 }
18252 }
18253
18254 var expressionType = getTypeForFilter(expression);
18255 var predicateFn;
18256 var matchAgainstAnyProp;
18257
18258 switch (expressionType) {
18259 case 'function':
18260 predicateFn = expression;
1482618261 break;
14827 case 'function':
14828 predicates.push(expression);
18262 case 'boolean':
18263 case 'null':
18264 case 'number':
18265 case 'string':
18266 matchAgainstAnyProp = true;
18267 //jshint -W086
18268 case 'object':
18269 //jshint +W086
18270 predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp);
1482918271 break;
1483018272 default:
1483118273 return array;
1483218274 }
14833 var filtered = [];
14834 for ( var j = 0; j < array.length; j++) {
14835 var value = array[j];
14836 if (predicates.check(value)) {
14837 filtered.push(value);
14838 }
14839 }
14840 return filtered;
18275
18276 return Array.prototype.filter.call(array, predicateFn);
1484118277 };
18278 }
18279
18280 // Helper functions for `filterFilter`
18281 function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
18282 var shouldMatchPrimitives = isObject(expression) && ('$' in expression);
18283 var predicateFn;
18284
18285 if (comparator === true) {
18286 comparator = equals;
18287 } else if (!isFunction(comparator)) {
18288 comparator = function(actual, expected) {
18289 if (isUndefined(actual)) {
18290 // No substring matching against `undefined`
18291 return false;
18292 }
18293 if ((actual === null) || (expected === null)) {
18294 // No substring matching against `null`; only match against `null`
18295 return actual === expected;
18296 }
18297 if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) {
18298 // Should not compare primitives against objects, unless they have custom `toString` method
18299 return false;
18300 }
18301
18302 actual = lowercase('' + actual);
18303 expected = lowercase('' + expected);
18304 return actual.indexOf(expected) !== -1;
18305 };
18306 }
18307
18308 predicateFn = function(item) {
18309 if (shouldMatchPrimitives && !isObject(item)) {
18310 return deepCompare(item, expression.$, comparator, false);
18311 }
18312 return deepCompare(item, expression, comparator, matchAgainstAnyProp);
18313 };
18314
18315 return predicateFn;
18316 }
18317
18318 function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) {
18319 var actualType = getTypeForFilter(actual);
18320 var expectedType = getTypeForFilter(expected);
18321
18322 if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
18323 return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp);
18324 } else if (isArray(actual)) {
18325 // In case `actual` is an array, consider it a match
18326 // if ANY of it's items matches `expected`
18327 return actual.some(function(item) {
18328 return deepCompare(item, expected, comparator, matchAgainstAnyProp);
18329 });
18330 }
18331
18332 switch (actualType) {
18333 case 'object':
18334 var key;
18335 if (matchAgainstAnyProp) {
18336 for (key in actual) {
18337 if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) {
18338 return true;
18339 }
18340 }
18341 return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false);
18342 } else if (expectedType === 'object') {
18343 for (key in expected) {
18344 var expectedVal = expected[key];
18345 if (isFunction(expectedVal) || isUndefined(expectedVal)) {
18346 continue;
18347 }
18348
18349 var matchAnyProperty = key === '$';
18350 var actualVal = matchAnyProperty ? actual : actual[key];
18351 if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) {
18352 return false;
18353 }
18354 }
18355 return true;
18356 } else {
18357 return comparator(actual, expected);
18358 }
18359 break;
18360 case 'function':
18361 return false;
18362 default:
18363 return comparator(actual, expected);
18364 }
18365 }
18366
18367 // Used for easily differentiating between `null` and actual `object`
18368 function getTypeForFilter(val) {
18369 return (val === null) ? 'null' : typeof val;
1484218370 }
1484318371
1484418372 /**
1485218380 *
1485318381 * @param {number} amount Input to filter.
1485418382 * @param {string=} symbol Currency symbol or identifier to be displayed.
18383 * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale
1485518384 * @returns {string} Formatted number.
1485618385 *
1485718386 *
1486518394 }]);
1486618395 </script>
1486718396 <div ng-controller="ExampleController">
14868 <input type="number" ng-model="amount"> <br>
18397 <input type="number" ng-model="amount" aria-label="amount"> <br>
1486918398 default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br>
14870 custom currency identifier (USD$): <span>{{amount | currency:"USD$"}}</span>
18399 custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span>
18400 no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span>
1487118401 </div>
1487218402 </file>
1487318403 <file name="protractor.js" type="protractor">
1487418404 it('should init with 1234.56', function() {
1487518405 expect(element(by.id('currency-default')).getText()).toBe('$1,234.56');
14876 expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56');
18406 expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56');
18407 expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235');
1487718408 });
1487818409 it('should update', function() {
1487918410 if (browser.params.browser == 'safari') {
1488418415 element(by.model('amount')).clear();
1488518416 element(by.model('amount')).sendKeys('-1234');
1488618417 expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)');
14887 expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('(USD$1,234.00)');
18418 expect(element(by.id('currency-custom')).getText()).toBe('(USD$1,234.00)');
18419 expect(element(by.id('currency-no-fractions')).getText()).toBe('(USD$1,234)');
1488818420 });
1488918421 </file>
1489018422 </example>
1489218424 currencyFilter.$inject = ['$locale'];
1489318425 function currencyFilter($locale) {
1489418426 var formats = $locale.NUMBER_FORMATS;
14895 return function(amount, currencySymbol){
14896 if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM;
14897 return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2).
14898 replace(/\u00A4/g, currencySymbol);
18427 return function(amount, currencySymbol, fractionSize) {
18428 if (isUndefined(currencySymbol)) {
18429 currencySymbol = formats.CURRENCY_SYM;
18430 }
18431
18432 if (isUndefined(fractionSize)) {
18433 fractionSize = formats.PATTERNS[1].maxFrac;
18434 }
18435
18436 // if null or undefined pass it through
18437 return (amount == null)
18438 ? amount
18439 : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize).
18440 replace(/\u00A4/g, currencySymbol);
1489918441 };
1490018442 }
1490118443
1490718449 * @description
1490818450 * Formats a number as text.
1490918451 *
18452 * If the input is null or undefined, it will just be returned.
18453 * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned.
1491018454 * If the input is not a number an empty string is returned.
18455 *
1491118456 *
1491218457 * @param {number|string} number Number to format.
1491318458 * @param {(number|string)=} fractionSize Number of decimal places to round the number to.
1492518470 }]);
1492618471 </script>
1492718472 <div ng-controller="ExampleController">
14928 Enter number: <input ng-model='val'><br>
18473 <label>Enter number: <input ng-model='val'></label><br>
1492918474 Default formatting: <span id='number-default'>{{val | number}}</span><br>
1493018475 No fractions: <span>{{val | number:0}}</span><br>
1493118476 Negative number: <span>{{-val | number:4}}</span>
1495418499 function numberFilter($locale) {
1495518500 var formats = $locale.NUMBER_FORMATS;
1495618501 return function(number, fractionSize) {
14957 return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,
14958 fractionSize);
18502
18503 // if null or undefined pass it through
18504 return (number == null)
18505 ? number
18506 : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,
18507 fractionSize);
1495918508 };
1496018509 }
1496118510
1496218511 var DECIMAL_SEP = '.';
1496318512 function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
14964 if (number == null || !isFinite(number) || isObject(number)) return '';
18513 if (isObject(number)) return '';
1496518514
1496618515 var isNegative = number < 0;
1496718516 number = Math.abs(number);
18517
18518 var isInfinity = number === Infinity;
18519 if (!isInfinity && !isFinite(number)) return '';
18520
1496818521 var numStr = number + '',
1496918522 formatedText = '',
18523 hasExponent = false,
1497018524 parts = [];
1497118525
14972 var hasExponent = false;
14973 if (numStr.indexOf('e') !== -1) {
18526 if (isInfinity) formatedText = '\u221e';
18527
18528 if (!isInfinity && numStr.indexOf('e') !== -1) {
1497418529 var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
1497518530 if (match && match[2] == '-' && match[3] > fractionSize + 1) {
14976 numStr = '0';
1497718531 number = 0;
1497818532 } else {
1497918533 formatedText = numStr;
1498118535 }
1498218536 }
1498318537
14984 if (!hasExponent) {
18538 if (!isInfinity && !hasExponent) {
1498518539 var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
1498618540
1498718541 // determine fractionSize if it is not specified
1500518559 if (whole.length >= (lgroup + group)) {
1500618560 pos = whole.length - lgroup;
1500718561 for (i = 0; i < pos; i++) {
15008 if ((pos - i)%group === 0 && i !== 0) {
18562 if ((pos - i) % group === 0 && i !== 0) {
1500918563 formatedText += groupSep;
1501018564 }
1501118565 formatedText += whole.charAt(i);
1501318567 }
1501418568
1501518569 for (i = pos; i < whole.length; i++) {
15016 if ((whole.length - i)%lgroup === 0 && i !== 0) {
18570 if ((whole.length - i) % lgroup === 0 && i !== 0) {
1501718571 formatedText += groupSep;
1501818572 }
1501918573 formatedText += whole.charAt(i);
1502018574 }
1502118575
1502218576 // format fraction part.
15023 while(fraction.length < fractionSize) {
18577 while (fraction.length < fractionSize) {
1502418578 fraction += '0';
1502518579 }
1502618580
1502718581 if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
1502818582 } else {
15029
15030 if (fractionSize > 0 && number > -1 && number < 1) {
18583 if (fractionSize > 0 && number < 1) {
1503118584 formatedText = number.toFixed(fractionSize);
18585 number = parseFloat(formatedText);
1503218586 }
1503318587 }
1503418588
15035 parts.push(isNegative ? pattern.negPre : pattern.posPre);
15036 parts.push(formatedText);
15037 parts.push(isNegative ? pattern.negSuf : pattern.posSuf);
18589 if (number === 0) {
18590 isNegative = false;
18591 }
18592
18593 parts.push(isNegative ? pattern.negPre : pattern.posPre,
18594 formatedText,
18595 isNegative ? pattern.negSuf : pattern.posSuf);
1503818596 return parts.join('');
1503918597 }
1504018598
1504518603 num = -num;
1504618604 }
1504718605 num = '' + num;
15048 while(num.length < digits) num = '0' + num;
15049 if (trim)
18606 while (num.length < digits) num = '0' + num;
18607 if (trim) {
1505018608 num = num.substr(num.length - digits);
18609 }
1505118610 return neg + num;
1505218611 }
1505318612
1505618615 offset = offset || 0;
1505718616 return function(date) {
1505818617 var value = date['get' + name]();
15059 if (offset > 0 || value > -offset)
18618 if (offset > 0 || value > -offset) {
1506018619 value += offset;
15061 if (value === 0 && offset == -12 ) value = 12;
18620 }
18621 if (value === 0 && offset == -12) value = 12;
1506218622 return padNumber(value, size, trim);
1506318623 };
1506418624 }
1507218632 };
1507318633 }
1507418634
15075 function timeZoneGetter(date) {
15076 var zone = -1 * date.getTimezoneOffset();
18635 function timeZoneGetter(date, formats, offset) {
18636 var zone = -1 * offset;
1507718637 var paddedZone = (zone >= 0) ? "+" : "";
1507818638
1507918639 paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
1508218642 return paddedZone;
1508318643 }
1508418644
18645 function getFirstThursdayOfYear(year) {
18646 // 0 = index of January
18647 var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay();
18648 // 4 = index of Thursday (+1 to account for 1st = 5)
18649 // 11 = index of *next* Thursday (+1 account for 1st = 12)
18650 return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst);
18651 }
18652
18653 function getThursdayThisWeek(datetime) {
18654 return new Date(datetime.getFullYear(), datetime.getMonth(),
18655 // 4 = index of Thursday
18656 datetime.getDate() + (4 - datetime.getDay()));
18657 }
18658
18659 function weekGetter(size) {
18660 return function(date) {
18661 var firstThurs = getFirstThursdayOfYear(date.getFullYear()),
18662 thisThurs = getThursdayThisWeek(date);
18663
18664 var diff = +thisThurs - +firstThurs,
18665 result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week
18666
18667 return padNumber(result, size);
18668 };
18669 }
18670
1508518671 function ampmGetter(date, formats) {
1508618672 return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
18673 }
18674
18675 function eraGetter(date, formats) {
18676 return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1];
18677 }
18678
18679 function longEraGetter(date, formats) {
18680 return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1];
1508718681 }
1508818682
1508918683 var DATE_FORMATS = {
1511018704 EEEE: dateStrGetter('Day'),
1511118705 EEE: dateStrGetter('Day', true),
1511218706 a: ampmGetter,
15113 Z: timeZoneGetter
18707 Z: timeZoneGetter,
18708 ww: weekGetter(2),
18709 w: weekGetter(1),
18710 G: eraGetter,
18711 GG: eraGetter,
18712 GGG: eraGetter,
18713 GGGG: longEraGetter
1511418714 };
1511518715
15116 var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,
18716 var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
1511718717 NUMBER_STRING = /^\-?\d+$/;
1511818718
1511918719 /**
1513918739 * * `'EEE'`: Day in Week, (Sun-Sat)
1514018740 * * `'HH'`: Hour in day, padded (00-23)
1514118741 * * `'H'`: Hour in day (0-23)
15142 * * `'hh'`: Hour in am/pm, padded (01-12)
15143 * * `'h'`: Hour in am/pm, (1-12)
18742 * * `'hh'`: Hour in AM/PM, padded (01-12)
18743 * * `'h'`: Hour in AM/PM, (1-12)
1514418744 * * `'mm'`: Minute in hour, padded (00-59)
1514518745 * * `'m'`: Minute in hour (0-59)
1514618746 * * `'ss'`: Second in minute, padded (00-59)
1514718747 * * `'s'`: Second in minute (0-59)
15148 * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999)
15149 * * `'a'`: am/pm marker
18748 * * `'sss'`: Millisecond in second, padded (000-999)
18749 * * `'a'`: AM/PM marker
1515018750 * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200)
18751 * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year
18752 * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year
18753 * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD')
18754 * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini')
1515118755 *
1515218756 * `format` string can also be one of the following predefined
1515318757 * {@link guide/i18n localizable formats}:
1515418758 *
1515518759 * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale
15156 * (e.g. Sep 3, 2010 12:05:08 pm)
15157 * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm)
15158 * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale
18760 * (e.g. Sep 3, 2010 12:05:08 PM)
18761 * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM)
18762 * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale
1515918763 * (e.g. Friday, September 3, 2010)
1516018764 * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010)
1516118765 * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010)
1516218766 * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10)
15163 * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm)
15164 * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm)
15165 *
15166 * `format` string can contain literal values. These need to be quoted with single quotes (e.g.
15167 * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence
18767 * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM)
18768 * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM)
18769 *
18770 * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g.
18771 * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence
1516818772 * (e.g. `"h 'o''clock'"`).
1516918773 *
1517018774 * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
1517318777 * specified in the string input, the time is considered to be in the local timezone.
1517418778 * @param {string=} format Formatting rules (see Description). If not specified,
1517518779 * `mediumDate` is used.
18780 * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the
18781 * continental US time zone abbreviations, but for general use, use a time zone offset, for
18782 * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
18783 * If not specified, the timezone of the browser will be used.
1517618784 * @returns {string} Formatted string or the input if input is not recognized as date/millis.
1517718785 *
1517818786 * @example
1518418792 <span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br>
1518518793 <span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>:
1518618794 <span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br>
18795 <span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</span>:
18796 <span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br>
1518718797 </file>
1518818798 <file name="protractor.js" type="protractor">
1518918799 it('should format date', function() {
1519318803 toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/);
1519418804 expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
1519518805 toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
18806 expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
18807 toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/);
1519618808 });
1519718809 </file>
1519818810 </example>
1521318825 timeSetter = match[8] ? date.setUTCHours : date.setHours;
1521418826
1521518827 if (match[9]) {
15216 tzHour = int(match[9] + match[10]);
15217 tzMin = int(match[9] + match[11]);
15218 }
15219 dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3]));
15220 var h = int(match[4]||0) - tzHour;
15221 var m = int(match[5]||0) - tzMin;
15222 var s = int(match[6]||0);
15223 var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000);
18828 tzHour = toInt(match[9] + match[10]);
18829 tzMin = toInt(match[9] + match[11]);
18830 }
18831 dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
18832 var h = toInt(match[4] || 0) - tzHour;
18833 var m = toInt(match[5] || 0) - tzMin;
18834 var s = toInt(match[6] || 0);
18835 var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
1522418836 timeSetter.call(date, h, m, s, ms);
1522518837 return date;
1522618838 }
1522818840 }
1522918841
1523018842
15231 return function(date, format) {
18843 return function(date, format, timezone) {
1523218844 var text = '',
1523318845 parts = [],
1523418846 fn, match;
1523618848 format = format || 'mediumDate';
1523718849 format = $locale.DATETIME_FORMATS[format] || format;
1523818850 if (isString(date)) {
15239 date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date);
18851 date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date);
1524018852 }
1524118853
1524218854 if (isNumber(date)) {
1524318855 date = new Date(date);
1524418856 }
1524518857
15246 if (!isDate(date)) {
18858 if (!isDate(date) || !isFinite(date.getTime())) {
1524718859 return date;
1524818860 }
1524918861
15250 while(format) {
18862 while (format) {
1525118863 match = DATE_FORMATS_SPLIT.exec(format);
1525218864 if (match) {
1525318865 parts = concat(parts, match, 1);
1525818870 }
1525918871 }
1526018872
15261 forEach(parts, function(value){
18873 var dateTimezoneOffset = date.getTimezoneOffset();
18874 if (timezone) {
18875 dateTimezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
18876 date = convertTimezoneToLocal(date, timezone, true);
18877 }
18878 forEach(parts, function(value) {
1526218879 fn = DATE_FORMATS[value];
15263 text += fn ? fn(date, $locale.DATETIME_FORMATS)
18880 text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
1526418881 : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
1526518882 });
1526618883
1528118898 * the binding is automatically converted to JSON.
1528218899 *
1528318900 * @param {*} object Any JavaScript object (including arrays and primitive types) to filter.
18901 * @param {number=} spacing The number of spaces to use per indentation, defaults to 2.
1528418902 * @returns {string} JSON string.
1528518903 *
1528618904 *
1528718905 * @example
1528818906 <example>
1528918907 <file name="index.html">
15290 <pre>{{ {'name':'value'} | json }}</pre>
18908 <pre id="default-spacing">{{ {'name':'value'} | json }}</pre>
18909 <pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre>
1529118910 </file>
1529218911 <file name="protractor.js" type="protractor">
1529318912 it('should jsonify filtered objects', function() {
15294 expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/);
18913 expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
18914 expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
1529518915 });
1529618916 </file>
1529718917 </example>
1529818918 *
1529918919 */
1530018920 function jsonFilter() {
15301 return function(object) {
15302 return toJson(object, true);
18921 return function(object, spacing) {
18922 if (isUndefined(spacing)) {
18923 spacing = 2;
18924 }
18925 return toJson(object, spacing);
1530318926 };
1530418927 }
1530518928
1533218955 *
1533318956 * @description
1533418957 * Creates a new array or string containing only a specified number of elements. The elements
15335 * are taken from either the beginning or the end of the source array or string, as specified by
15336 * the value and sign (positive or negative) of `limit`.
15337 *
15338 * @param {Array|string} input Source array or string to be limited.
18958 * are taken from either the beginning or the end of the source array, string or number, as specified by
18959 * the value and sign (positive or negative) of `limit`. If a number is used as input, it is
18960 * converted to a string.
18961 *
18962 * @param {Array|string|number} input Source array, string or number to be limited.
1533918963 * @param {string|number} limit The length of the returned array or string. If the `limit` number
1534018964 * is positive, `limit` number of items from the beginning of the source array/string are copied.
1534118965 * If the number is negative, `limit` number of items from the end of the source array/string
15342 * are copied. The `limit` will be trimmed if it exceeds `array.length`
18966 * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined,
18967 * the input will be returned unchanged.
18968 * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin`
18969 * indicates an offset from the end of `input`. Defaults to `0`.
1534318970 * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array
1534418971 * had less than `limit` elements.
1534518972 *
1535118978 .controller('ExampleController', ['$scope', function($scope) {
1535218979 $scope.numbers = [1,2,3,4,5,6,7,8,9];
1535318980 $scope.letters = "abcdefghi";
18981 $scope.longNumber = 2345432342;
1535418982 $scope.numLimit = 3;
1535518983 $scope.letterLimit = 3;
18984 $scope.longNumberLimit = 3;
1535618985 }]);
1535718986 </script>
1535818987 <div ng-controller="ExampleController">
15359 Limit {{numbers}} to: <input type="integer" ng-model="numLimit">
18988 <label>
18989 Limit {{numbers}} to:
18990 <input type="number" step="1" ng-model="numLimit">
18991 </label>
1536018992 <p>Output numbers: {{ numbers | limitTo:numLimit }}</p>
15361 Limit {{letters}} to: <input type="integer" ng-model="letterLimit">
18993 <label>
18994 Limit {{letters}} to:
18995 <input type="number" step="1" ng-model="letterLimit">
18996 </label>
1536218997 <p>Output letters: {{ letters | limitTo:letterLimit }}</p>
18998 <label>
18999 Limit {{longNumber}} to:
19000 <input type="number" step="1" ng-model="longNumberLimit">
19001 </label>
19002 <p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p>
1536319003 </div>
1536419004 </file>
1536519005 <file name="protractor.js" type="protractor">
1536619006 var numLimitInput = element(by.model('numLimit'));
1536719007 var letterLimitInput = element(by.model('letterLimit'));
19008 var longNumberLimitInput = element(by.model('longNumberLimit'));
1536819009 var limitedNumbers = element(by.binding('numbers | limitTo:numLimit'));
1536919010 var limitedLetters = element(by.binding('letters | limitTo:letterLimit'));
19011 var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit'));
1537019012
1537119013 it('should limit the number array to first three items', function() {
1537219014 expect(numLimitInput.getAttribute('value')).toBe('3');
1537319015 expect(letterLimitInput.getAttribute('value')).toBe('3');
19016 expect(longNumberLimitInput.getAttribute('value')).toBe('3');
1537419017 expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]');
1537519018 expect(limitedLetters.getText()).toEqual('Output letters: abc');
19019 expect(limitedLongNumber.getText()).toEqual('Output long number: 234');
1537619020 });
1537719021
15378 it('should update the output when -3 is entered', function() {
15379 numLimitInput.clear();
15380 numLimitInput.sendKeys('-3');
15381 letterLimitInput.clear();
15382 letterLimitInput.sendKeys('-3');
15383 expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]');
15384 expect(limitedLetters.getText()).toEqual('Output letters: ghi');
15385 });
19022 // There is a bug in safari and protractor that doesn't like the minus key
19023 // it('should update the output when -3 is entered', function() {
19024 // numLimitInput.clear();
19025 // numLimitInput.sendKeys('-3');
19026 // letterLimitInput.clear();
19027 // letterLimitInput.sendKeys('-3');
19028 // longNumberLimitInput.clear();
19029 // longNumberLimitInput.sendKeys('-3');
19030 // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]');
19031 // expect(limitedLetters.getText()).toEqual('Output letters: ghi');
19032 // expect(limitedLongNumber.getText()).toEqual('Output long number: 342');
19033 // });
1538619034
1538719035 it('should not exceed the maximum size of input array', function() {
1538819036 numLimitInput.clear();
1538919037 numLimitInput.sendKeys('100');
1539019038 letterLimitInput.clear();
1539119039 letterLimitInput.sendKeys('100');
19040 longNumberLimitInput.clear();
19041 longNumberLimitInput.sendKeys('100');
1539219042 expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]');
1539319043 expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi');
19044 expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342');
1539419045 });
1539519046 </file>
1539619047 </example>
15397 */
15398 function limitToFilter(){
15399 return function(input, limit) {
15400 if (!isArray(input) && !isString(input)) return input;
15401
19048 */
19049 function limitToFilter() {
19050 return function(input, limit, begin) {
1540219051 if (Math.abs(Number(limit)) === Infinity) {
1540319052 limit = Number(limit);
1540419053 } else {
15405 limit = int(limit);
15406 }
15407
15408 if (isString(input)) {
15409 //NaN check on limit
15410 if (limit) {
15411 return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length);
19054 limit = toInt(limit);
19055 }
19056 if (isNaN(limit)) return input;
19057
19058 if (isNumber(input)) input = input.toString();
19059 if (!isArray(input) && !isString(input)) return input;
19060
19061 begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
19062 begin = (begin < 0 && begin >= -input.length) ? input.length + begin : begin;
19063
19064 if (limit >= 0) {
19065 return input.slice(begin, begin + limit);
19066 } else {
19067 if (begin === 0) {
19068 return input.slice(limit, input.length);
1541219069 } else {
15413 return "";
15414 }
15415 }
15416
15417 var out = [],
15418 i, n;
15419
15420 // if abs(limit) exceeds maximum length, trim it
15421 if (limit > input.length)
15422 limit = input.length;
15423 else if (limit < -input.length)
15424 limit = -input.length;
15425
15426 if (limit > 0) {
15427 i = 0;
15428 n = limit;
15429 } else {
15430 i = input.length + limit;
15431 n = input.length;
15432 }
15433
15434 for (; i<n; i++) {
15435 out.push(input[i]);
15436 }
15437
15438 return out;
19070 return input.slice(Math.max(0, begin + limit), begin);
19071 }
19072 }
1543919073 };
1544019074 }
1544119075
1544719081 * @description
1544819082 * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically
1544919083 * for strings and numerically for numbers. Note: if you notice numbers are not being sorted
15450 * correctly, make sure they are actually being saved as numbers and not strings.
19084 * as expected, make sure they are actually being saved as numbers and not strings.
1545119085 *
1545219086 * @param {Array} array The array to sort.
15453 * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be
19087 * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be
1545419088 * used by the comparator to determine the order of elements.
1545519089 *
1545619090 * Can be one of:
1545719091 *
1545819092 * - `function`: Getter function. The result of this function will be sorted using the
15459 * `<`, `=`, `>` operator.
19093 * `<`, `===`, `>` operator.
1546019094 * - `string`: An Angular expression. The result of this expression is used to compare elements
1546119095 * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by
1546219096 * 3 first characters of a property called `name`). The result of a constant expression
1546319097 * is interpreted as a property name to be used in comparisons (for example `"special name"`
1546419098 * to sort object by the value of their `special name` property). An expression can be
1546519099 * optionally prefixed with `+` or `-` to control ascending or descending sort order
15466 * (for example, `+name` or `-name`).
19100 * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array
19101 * element itself is used to compare where sorting.
1546719102 * - `Array`: An array of function or string predicates. The first predicate in the array
1546819103 * is used for sorting, but when two items are equivalent, the next predicate is used.
1546919104 *
19105 * If the predicate is missing or empty then it defaults to `'+'`.
19106 *
1547019107 * @param {boolean=} reverse Reverse the order of the array.
1547119108 * @returns {Array} Sorted copy of the source array.
1547219109 *
19110 *
19111 * @example
19112 * The example below demonstrates a simple ngRepeat, where the data is sorted
19113 * by age in descending order (predicate is set to `'-age'`).
19114 * `reverse` is not set, which means it defaults to `false`.
19115 <example module="orderByExample">
19116 <file name="index.html">
19117 <script>
19118 angular.module('orderByExample', [])
19119 .controller('ExampleController', ['$scope', function($scope) {
19120 $scope.friends =
19121 [{name:'John', phone:'555-1212', age:10},
19122 {name:'Mary', phone:'555-9876', age:19},
19123 {name:'Mike', phone:'555-4321', age:21},
19124 {name:'Adam', phone:'555-5678', age:35},
19125 {name:'Julie', phone:'555-8765', age:29}];
19126 }]);
19127 </script>
19128 <div ng-controller="ExampleController">
19129 <table class="friend">
19130 <tr>
19131 <th>Name</th>
19132 <th>Phone Number</th>
19133 <th>Age</th>
19134 </tr>
19135 <tr ng-repeat="friend in friends | orderBy:'-age'">
19136 <td>{{friend.name}}</td>
19137 <td>{{friend.phone}}</td>
19138 <td>{{friend.age}}</td>
19139 </tr>
19140 </table>
19141 </div>
19142 </file>
19143 </example>
19144 *
19145 * The predicate and reverse parameters can be controlled dynamically through scope properties,
19146 * as shown in the next example.
1547319147 * @example
1547419148 <example module="orderByExample">
1547519149 <file name="index.html">
1548219156 {name:'Mike', phone:'555-4321', age:21},
1548319157 {name:'Adam', phone:'555-5678', age:35},
1548419158 {name:'Julie', phone:'555-8765', age:29}];
15485 $scope.predicate = '-age';
19159 $scope.predicate = 'age';
19160 $scope.reverse = true;
19161 $scope.order = function(predicate) {
19162 $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
19163 $scope.predicate = predicate;
19164 };
1548619165 }]);
1548719166 </script>
19167 <style type="text/css">
19168 .sortorder:after {
19169 content: '\25b2';
19170 }
19171 .sortorder.reverse:after {
19172 content: '\25bc';
19173 }
19174 </style>
1548819175 <div ng-controller="ExampleController">
1548919176 <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
1549019177 <hr/>
1549119178 [ <a href="" ng-click="predicate=''">unsorted</a> ]
1549219179 <table class="friend">
1549319180 <tr>
15494 <th><a href="" ng-click="predicate = 'name'; reverse=false">Name</a>
15495 (<a href="" ng-click="predicate = '-name'; reverse=false">^</a>)</th>
15496 <th><a href="" ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th>
15497 <th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</a></th>
19181 <th>
19182 <a href="" ng-click="order('name')">Name</a>
19183 <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
19184 </th>
19185 <th>
19186 <a href="" ng-click="order('phone')">Phone Number</a>
19187 <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
19188 </th>
19189 <th>
19190 <a href="" ng-click="order('age')">Age</a>
19191 <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
19192 </th>
1549819193 </tr>
1549919194 <tr ng-repeat="friend in friends | orderBy:predicate:reverse">
1550019195 <td>{{friend.name}}</td>
1555219247 </example>
1555319248 */
1555419249 orderByFilter.$inject = ['$parse'];
15555 function orderByFilter($parse){
19250 function orderByFilter($parse) {
1555619251 return function(array, sortPredicate, reverseOrder) {
15557 if (!isArray(array)) return array;
15558 if (!sortPredicate) return array;
15559 sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate];
15560 sortPredicate = map(sortPredicate, function(predicate){
15561 var descending = false, get = predicate || identity;
15562 if (isString(predicate)) {
19252
19253 if (!(isArrayLike(array))) return array;
19254
19255 if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
19256 if (sortPredicate.length === 0) { sortPredicate = ['+']; }
19257
19258 var predicates = processPredicates(sortPredicate, reverseOrder);
19259
19260 // The next three lines are a version of a Swartzian Transform idiom from Perl
19261 // (sometimes called the Decorate-Sort-Undecorate idiom)
19262 // See https://en.wikipedia.org/wiki/Schwartzian_transform
19263 var compareValues = Array.prototype.map.call(array, getComparisonObject);
19264 compareValues.sort(doComparison);
19265 array = compareValues.map(function(item) { return item.value; });
19266
19267 return array;
19268
19269 function getComparisonObject(value, index) {
19270 return {
19271 value: value,
19272 predicateValues: predicates.map(function(predicate) {
19273 return getPredicateValue(predicate.get(value), index);
19274 })
19275 };
19276 }
19277
19278 function doComparison(v1, v2) {
19279 var result = 0;
19280 for (var index=0, length = predicates.length; index < length; ++index) {
19281 result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending;
19282 if (result) break;
19283 }
19284 return result;
19285 }
19286 };
19287
19288 function processPredicates(sortPredicate, reverseOrder) {
19289 reverseOrder = reverseOrder ? -1 : 1;
19290 return sortPredicate.map(function(predicate) {
19291 var descending = 1, get = identity;
19292
19293 if (isFunction(predicate)) {
19294 get = predicate;
19295 } else if (isString(predicate)) {
1556319296 if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
15564 descending = predicate.charAt(0) == '-';
19297 descending = predicate.charAt(0) == '-' ? -1 : 1;
1556519298 predicate = predicate.substring(1);
1556619299 }
15567 get = $parse(predicate);
15568 if (get.constant) {
15569 var key = get();
15570 return reverseComparator(function(a,b) {
15571 return compare(a[key], b[key]);
15572 }, descending);
19300 if (predicate !== '') {
19301 get = $parse(predicate);
19302 if (get.constant) {
19303 var key = get();
19304 get = function(value) { return value[key]; };
19305 }
1557319306 }
1557419307 }
15575 return reverseComparator(function(a,b){
15576 return compare(get(a),get(b));
15577 }, descending);
19308 return { get: get, descending: descending * reverseOrder };
1557819309 });
15579 var arrayCopy = [];
15580 for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); }
15581 return arrayCopy.sort(reverseComparator(comparator, reverseOrder));
15582
15583 function comparator(o1, o2){
15584 for ( var i = 0; i < sortPredicate.length; i++) {
15585 var comp = sortPredicate[i](o1, o2);
15586 if (comp !== 0) return comp;
15587 }
15588 return 0;
15589 }
15590 function reverseComparator(comp, descending) {
15591 return toBoolean(descending)
15592 ? function(a,b){return comp(b,a);}
15593 : comp;
15594 }
15595 function compare(v1, v2){
15596 var t1 = typeof v1;
15597 var t2 = typeof v2;
15598 if (t1 == t2) {
15599 if (isDate(v1) && isDate(v2)) {
15600 v1 = v1.valueOf();
15601 v2 = v2.valueOf();
15602 }
15603 if (t1 == "string") {
15604 v1 = v1.toLowerCase();
15605 v2 = v2.toLowerCase();
15606 }
15607 if (v1 === v2) return 0;
15608 return v1 < v2 ? -1 : 1;
15609 } else {
15610 return t1 < t2 ? -1 : 1;
15611 }
15612 }
15613 };
19310 }
19311
19312 function isPrimitive(value) {
19313 switch (typeof value) {
19314 case 'number': /* falls through */
19315 case 'boolean': /* falls through */
19316 case 'string':
19317 return true;
19318 default:
19319 return false;
19320 }
19321 }
19322
19323 function objectValue(value, index) {
19324 // If `valueOf` is a valid function use that
19325 if (typeof value.valueOf === 'function') {
19326 value = value.valueOf();
19327 if (isPrimitive(value)) return value;
19328 }
19329 // If `toString` is a valid function and not the one from `Object.prototype` use that
19330 if (hasCustomToString(value)) {
19331 value = value.toString();
19332 if (isPrimitive(value)) return value;
19333 }
19334 // We have a basic object so we use the position of the object in the collection
19335 return index;
19336 }
19337
19338 function getPredicateValue(value, index) {
19339 var type = typeof value;
19340 if (value === null) {
19341 type = 'string';
19342 value = 'null';
19343 } else if (type === 'string') {
19344 value = value.toLowerCase();
19345 } else if (type === 'object') {
19346 value = objectValue(value, index);
19347 }
19348 return { value: value, type: type };
19349 }
19350
19351 function compare(v1, v2) {
19352 var result = 0;
19353 if (v1.type === v2.type) {
19354 if (v1.value !== v2.value) {
19355 result = v1.value < v2.value ? -1 : 1;
19356 }
19357 } else {
19358 result = v1.type < v2.type ? -1 : 1;
19359 }
19360 return result;
19361 }
1561419362 }
1561519363
1561619364 function ngDirective(directive) {
1563919387 var htmlAnchorDirective = valueFn({
1564019388 restrict: 'E',
1564119389 compile: function(element, attr) {
15642
15643 if (msie <= 8) {
15644
15645 // turn <a href ng-click="..">link</a> into a stylable link in IE
15646 // but only if it doesn't have name attribute, in which case it's an anchor
15647 if (!attr.href && !attr.name) {
15648 attr.$set('href', '');
15649 }
15650
15651 // add a comment node to anchors to workaround IE bug that causes element content to be reset
15652 // to new attribute content if attribute is updated with value containing @ and element also
15653 // contains value with @
15654 // see issue #1949
15655 element.append(document.createComment('IE fix'));
15656 }
15657
15658 if (!attr.href && !attr.xlinkHref && !attr.name) {
19390 if (!attr.href && !attr.xlinkHref) {
1565919391 return function(scope, element) {
19392 // If the linked element is not an anchor tag anymore, do nothing
19393 if (element[0].nodeName.toLowerCase() !== 'a') return;
19394
1566019395 // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
1566119396 var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
1566219397 'xlink:href' : 'href';
15663 element.on('click', function(event){
19398 element.on('click', function(event) {
1566419399 // if we have no href url, then don't navigate anywhere.
1566519400 if (!element.attr(href)) {
1566619401 event.preventDefault();
1568219417 * make the link go to the wrong URL if the user clicks it before
1568319418 * Angular has a chance to replace the `{{hash}}` markup with its
1568419419 * value. Until Angular replaces the markup the link will be broken
15685 * and will most likely return a 404 error.
15686 *
15687 * The `ngHref` directive solves this problem.
19420 * and will most likely return a 404 error. The `ngHref` directive
19421 * solves this problem.
1568819422 *
1568919423 * The wrong way to write it:
1569019424 * ```html
15691 * <a href="http://www.gravatar.com/avatar/{{hash}}"/>
19425 * <a href="http://www.gravatar.com/avatar/{{hash}}">link1</a>
1569219426 * ```
1569319427 *
1569419428 * The correct way to write it:
1569519429 * ```html
15696 * <a ng-href="http://www.gravatar.com/avatar/{{hash}}"/>
19430 * <a ng-href="http://www.gravatar.com/avatar/{{hash}}">link1</a>
1569719431 * ```
1569819432 *
1569919433 * @element A
1574019474 }, 5000, 'page should navigate to /123');
1574119475 });
1574219476
15743 xit('should execute ng-click but not reload when href empty string and name specified', function() {
19477 it('should execute ng-click but not reload when href empty string and name specified', function() {
1574419478 element(by.id('link-4')).click();
1574519479 expect(element(by.model('value')).getAttribute('value')).toEqual('4');
1574619480 expect(element(by.id('link-4')).getAttribute('href')).toBe('');
1578519519 *
1578619520 * The buggy way to write it:
1578719521 * ```html
15788 * <img src="http://www.gravatar.com/avatar/{{hash}}"/>
19522 * <img src="http://www.gravatar.com/avatar/{{hash}}" alt="Description"/>
1578919523 * ```
1579019524 *
1579119525 * The correct way to write it:
1579219526 * ```html
15793 * <img ng-src="http://www.gravatar.com/avatar/{{hash}}"/>
19527 * <img ng-src="http://www.gravatar.com/avatar/{{hash}}" alt="Description" />
1579419528 * ```
1579519529 *
1579619530 * @element IMG
1581119545 *
1581219546 * The buggy way to write it:
1581319547 * ```html
15814 * <img srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/>
19548 * <img srcset="http://www.gravatar.com/avatar/{{hash}} 2x" alt="Description"/>
1581519549 * ```
1581619550 *
1581719551 * The correct way to write it:
1581819552 * ```html
15819 * <img ng-srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/>
19553 * <img ng-srcset="http://www.gravatar.com/avatar/{{hash}} 2x" alt="Description" />
1582019554 * ```
1582119555 *
1582219556 * @element IMG
1583119565 *
1583219566 * @description
1583319567 *
15834 * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs:
19568 * This directive sets the `disabled` attribute on the element if the
19569 * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
19570 *
19571 * A special directive is necessary because we cannot use interpolation inside the `disabled`
19572 * attribute. The following example would make the button enabled on Chrome/Firefox
19573 * but not on older IEs:
19574 *
1583519575 * ```html
15836 * <div ng-init="scope = { isDisabled: false }">
15837 * <button disabled="{{scope.isDisabled}}">Disabled</button>
19576 * <!-- See below for an example of ng-disabled being used correctly -->
19577 * <div ng-init="isDisabled = false">
19578 * <button disabled="{{isDisabled}}">Disabled</button>
1583819579 * </div>
1583919580 * ```
1584019581 *
15841 * The HTML specification does not require browsers to preserve the values of boolean attributes
15842 * such as disabled. (Their presence means true and their absence means false.)
19582 * This is because the HTML specification does not require browsers to preserve the values of
19583 * boolean attributes such as `disabled` (Their presence means true and their absence means false.)
1584319584 * If we put an Angular interpolation expression into such an attribute then the
1584419585 * binding information would be lost when the browser removes the attribute.
15845 * The `ngDisabled` directive solves this problem for the `disabled` attribute.
15846 * This complementary directive is not removed by the browser and so provides
15847 * a permanent reliable place to store the binding information.
1584819586 *
1584919587 * @example
1585019588 <example>
1585119589 <file name="index.html">
15852 Click me to toggle: <input type="checkbox" ng-model="checked"><br/>
19590 <label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/>
1585319591 <button ng-model="button" ng-disabled="checked">Button</button>
1585419592 </file>
1585519593 <file name="protractor.js" type="protractor">
1586319601 *
1586419602 * @element INPUT
1586519603 * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
15866 * then special attribute "disabled" will be set on the element
19604 * then the `disabled` attribute will be set on the element
1586719605 */
1586819606
1586919607
1587419612 * @priority 100
1587519613 *
1587619614 * @description
19615 * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy.
19616 *
19617 * Note that this directive should not be used together with {@link ngModel `ngModel`},
19618 * as this can lead to unexpected behavior.
19619 *
19620 * ### Why do we need `ngChecked`?
19621 *
1587719622 * The HTML specification does not require browsers to preserve the values of boolean attributes
1587819623 * such as checked. (Their presence means true and their absence means false.)
1587919624 * If we put an Angular interpolation expression into such an attribute then the
1588419629 * @example
1588519630 <example>
1588619631 <file name="index.html">
15887 Check me to check both: <input type="checkbox" ng-model="master"><br/>
15888 <input id="checkSlave" type="checkbox" ng-checked="master">
19632 <label>Check me to check both: <input type="checkbox" ng-model="master"></label><br/>
19633 <input id="checkSlave" type="checkbox" ng-checked="master" aria-label="Slave input">
1588919634 </file>
1589019635 <file name="protractor.js" type="protractor">
1589119636 it('should check both checkBoxes', function() {
1589819643 *
1589919644 * @element INPUT
1590019645 * @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
15901 * then special attribute "checked" will be set on the element
19646 * then the `checked` attribute will be set on the element
1590219647 */
1590319648
1590419649
1591919664 * @example
1592019665 <example>
1592119666 <file name="index.html">
15922 Check me to make text readonly: <input type="checkbox" ng-model="checked"><br/>
15923 <input type="text" ng-readonly="checked" value="I'm Angular"/>
19667 <label>Check me to make text readonly: <input type="checkbox" ng-model="checked"></label><br/>
19668 <input type="text" ng-readonly="checked" value="I'm Angular" aria-label="Readonly field" />
1592419669 </file>
1592519670 <file name="protractor.js" type="protractor">
1592619671 it('should toggle readonly attr', function() {
1595519700 * @example
1595619701 <example>
1595719702 <file name="index.html">
15958 Check me to select: <input type="checkbox" ng-model="selected"><br/>
15959 <select>
19703 <label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/>
19704 <select aria-label="ngSelected demo">
1596019705 <option>Hello!</option>
1596119706 <option id="greet" ng-selected="selected">Greetings!</option>
1596219707 </select>
1599219737 * @example
1599319738 <example>
1599419739 <file name="index.html">
15995 Check me check multiple: <input type="checkbox" ng-model="open"><br/>
19740 <label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/>
1599619741 <details id="details" ng-open="open">
1599719742 <summary>Show/Hide me</summary>
1599819743 </details>
1601319758
1601419759 var ngAttributeAliasDirectives = {};
1601519760
16016
1601719761 // boolean attrs are evaluated
1601819762 forEach(BOOLEAN_ATTR, function(propName, attrName) {
1601919763 // binding to multiple is not supported
1602019764 if (propName == "multiple") return;
1602119765
19766 function defaultLinkFn(scope, element, attr) {
19767 scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
19768 attr.$set(attrName, !!value);
19769 });
19770 }
19771
1602219772 var normalized = directiveNormalize('ng-' + attrName);
19773 var linkFn = defaultLinkFn;
19774
19775 if (propName === 'checked') {
19776 linkFn = function(scope, element, attr) {
19777 // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input
19778 if (attr.ngModel !== attr[normalized]) {
19779 defaultLinkFn(scope, element, attr);
19780 }
19781 };
19782 }
19783
1602319784 ngAttributeAliasDirectives[normalized] = function() {
19785 return {
19786 restrict: 'A',
19787 priority: 100,
19788 link: linkFn
19789 };
19790 };
19791 });
19792
19793 // aliased input attrs are evaluated
19794 forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
19795 ngAttributeAliasDirectives[ngAttr] = function() {
1602419796 return {
1602519797 priority: 100,
1602619798 link: function(scope, element, attr) {
16027 scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
16028 attr.$set(attrName, !!value);
19799 //special case ngPattern when a literal regular expression value
19800 //is used as the expression (this way we don't have to watch anything).
19801 if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") {
19802 var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
19803 if (match) {
19804 attr.$set("ngPattern", new RegExp(match[1], match[2]));
19805 return;
19806 }
19807 }
19808
19809 scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) {
19810 attr.$set(ngAttr, value);
1602919811 });
1603019812 }
1603119813 };
1603219814 };
1603319815 });
16034
1603519816
1603619817 // ng-src, ng-srcset, ng-href are interpolated
1603719818 forEach(['src', 'srcset', 'href'], function(attrName) {
1607119852 };
1607219853 });
1607319854
16074 /* global -nullFormCtrl */
19855 /* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
19856 */
1607519857 var nullFormCtrl = {
1607619858 $addControl: noop,
19859 $$renameControl: nullFormRenameControl,
1607719860 $removeControl: noop,
1607819861 $setValidity: noop,
1607919862 $setDirty: noop,
16080 $setPristine: noop
16081 };
19863 $setPristine: noop,
19864 $setSubmitted: noop
19865 },
19866 SUBMITTED_CLASS = 'ng-submitted';
19867
19868 function nullFormRenameControl(control, name) {
19869 control.$name = name;
19870 }
1608219871
1608319872 /**
1608419873 * @ngdoc type
1608819877 * @property {boolean} $dirty True if user has already interacted with the form.
1608919878 * @property {boolean} $valid True if all of the containing forms and controls are valid.
1609019879 * @property {boolean} $invalid True if at least one containing control or form is invalid.
16091 *
16092 * @property {Object} $error Is an object hash, containing references to all invalid controls or
16093 * forms, where:
19880 * @property {boolean} $submitted True if user has submitted the form even if its invalid.
19881 *
19882 * @property {Object} $error Is an object hash, containing references to controls or
19883 * forms with failing validators, where:
1609419884 *
1609519885 * - keys are validation tokens (error names),
16096 * - values are arrays of controls or forms that are invalid for given error name.
16097 *
19886 * - values are arrays of controls or forms that have a failing validator for given error name.
1609819887 *
1609919888 * Built-in validation tokens:
1610019889 *
1610719896 * - `pattern`
1610819897 * - `required`
1610919898 * - `url`
19899 * - `date`
19900 * - `datetimelocal`
19901 * - `time`
19902 * - `week`
19903 * - `month`
1611019904 *
1611119905 * @description
1611219906 * `FormController` keeps track of all its controls and nested forms as well as the state of them,
1611719911 *
1611819912 */
1611919913 //asks for $scope to fool the BC controller module
16120 FormController.$inject = ['$element', '$attrs', '$scope', '$animate'];
16121 function FormController(element, attrs, $scope, $animate) {
19914 FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
19915 function FormController(element, attrs, $scope, $animate, $interpolate) {
1612219916 var form = this,
16123 parentForm = element.parent().controller('form') || nullFormCtrl,
16124 invalidCount = 0, // used to easily determine if we are valid
16125 errors = form.$error = {},
1612619917 controls = [];
1612719918
19919 var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
19920
1612819921 // init state
16129 form.$name = attrs.name || attrs.ngForm;
19922 form.$error = {};
19923 form.$$success = {};
19924 form.$pending = undefined;
19925 form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
1613019926 form.$dirty = false;
1613119927 form.$pristine = true;
1613219928 form.$valid = true;
1613319929 form.$invalid = false;
19930 form.$submitted = false;
1613419931
1613519932 parentForm.$addControl(form);
1613619933
16137 // Setup initial state of the control
16138 element.addClass(PRISTINE_CLASS);
16139 toggleValidCss(true);
16140
16141 // convenience method for easy toggling of classes
16142 function toggleValidCss(isValid, validationErrorKey) {
16143 validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
16144 $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
16145 $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
16146 }
19934 /**
19935 * @ngdoc method
19936 * @name form.FormController#$rollbackViewValue
19937 *
19938 * @description
19939 * Rollback all form controls pending updates to the `$modelValue`.
19940 *
19941 * Updates may be pending by a debounced event or because the input is waiting for a some future
19942 * event defined in `ng-model-options`. This method is typically needed by the reset button of
19943 * a form that uses `ng-model-options` to pend updates.
19944 */
19945 form.$rollbackViewValue = function() {
19946 forEach(controls, function(control) {
19947 control.$rollbackViewValue();
19948 });
19949 };
19950
19951 /**
19952 * @ngdoc method
19953 * @name form.FormController#$commitViewValue
19954 *
19955 * @description
19956 * Commit all form controls pending updates to the `$modelValue`.
19957 *
19958 * Updates may be pending by a debounced event or because the input is waiting for a some future
19959 * event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
19960 * usually handles calling this in response to input events.
19961 */
19962 form.$commitViewValue = function() {
19963 forEach(controls, function(control) {
19964 control.$commitViewValue();
19965 });
19966 };
1614719967
1614819968 /**
1614919969 * @ngdoc method
1616519985 }
1616619986 };
1616719987
19988 // Private API: rename a form control
19989 form.$$renameControl = function(control, newName) {
19990 var oldName = control.$name;
19991
19992 if (form[oldName] === control) {
19993 delete form[oldName];
19994 }
19995 form[newName] = control;
19996 control.$name = newName;
19997 };
19998
1616819999 /**
1616920000 * @ngdoc method
1617020001 * @name form.FormController#$removeControl
1617820009 if (control.$name && form[control.$name] === control) {
1617920010 delete form[control.$name];
1618020011 }
16181 forEach(errors, function(queue, validationToken) {
16182 form.$setValidity(validationToken, true, control);
20012 forEach(form.$pending, function(value, name) {
20013 form.$setValidity(name, null, control);
20014 });
20015 forEach(form.$error, function(value, name) {
20016 form.$setValidity(name, null, control);
20017 });
20018 forEach(form.$$success, function(value, name) {
20019 form.$setValidity(name, null, control);
1618320020 });
1618420021
1618520022 arrayRemove(controls, control);
1618620023 };
20024
1618720025
1618820026 /**
1618920027 * @ngdoc method
1619420032 *
1619520033 * This method will also propagate to parent forms.
1619620034 */
16197 form.$setValidity = function(validationToken, isValid, control) {
16198 var queue = errors[validationToken];
16199
16200 if (isValid) {
16201 if (queue) {
16202 arrayRemove(queue, control);
16203 if (!queue.length) {
16204 invalidCount--;
16205 if (!invalidCount) {
16206 toggleValidCss(isValid);
16207 form.$valid = true;
16208 form.$invalid = false;
16209 }
16210 errors[validationToken] = false;
16211 toggleValidCss(true, validationToken);
16212 parentForm.$setValidity(validationToken, true, form);
20035 addSetValidityMethod({
20036 ctrl: this,
20037 $element: element,
20038 set: function(object, property, controller) {
20039 var list = object[property];
20040 if (!list) {
20041 object[property] = [controller];
20042 } else {
20043 var index = list.indexOf(controller);
20044 if (index === -1) {
20045 list.push(controller);
1621320046 }
1621420047 }
16215
16216 } else {
16217 if (!invalidCount) {
16218 toggleValidCss(isValid);
16219 }
16220 if (queue) {
16221 if (includes(queue, control)) return;
16222 } else {
16223 errors[validationToken] = queue = [];
16224 invalidCount++;
16225 toggleValidCss(false, validationToken);
16226 parentForm.$setValidity(validationToken, false, form);
16227 }
16228 queue.push(control);
16229
16230 form.$valid = false;
16231 form.$invalid = true;
16232 }
16233 };
20048 },
20049 unset: function(object, property, controller) {
20050 var list = object[property];
20051 if (!list) {
20052 return;
20053 }
20054 arrayRemove(list, controller);
20055 if (list.length === 0) {
20056 delete object[property];
20057 }
20058 },
20059 parentForm: parentForm,
20060 $animate: $animate
20061 });
1623420062
1623520063 /**
1623620064 * @ngdoc method
1626420092 * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
1626520093 * saving or resetting it.
1626620094 */
16267 form.$setPristine = function () {
16268 $animate.removeClass(element, DIRTY_CLASS);
16269 $animate.addClass(element, PRISTINE_CLASS);
20095 form.$setPristine = function() {
20096 $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
1627020097 form.$dirty = false;
1627120098 form.$pristine = true;
20099 form.$submitted = false;
1627220100 forEach(controls, function(control) {
1627320101 control.$setPristine();
1627420102 });
1627520103 };
20104
20105 /**
20106 * @ngdoc method
20107 * @name form.FormController#$setUntouched
20108 *
20109 * @description
20110 * Sets the form to its untouched state.
20111 *
20112 * This method can be called to remove the 'ng-touched' class and set the form controls to their
20113 * untouched state (ng-untouched class).
20114 *
20115 * Setting a form controls back to their untouched state is often useful when setting the form
20116 * back to its pristine state.
20117 */
20118 form.$setUntouched = function() {
20119 forEach(controls, function(control) {
20120 control.$setUntouched();
20121 });
20122 };
20123
20124 /**
20125 * @ngdoc method
20126 * @name form.FormController#$setSubmitted
20127 *
20128 * @description
20129 * Sets the form to its submitted state.
20130 */
20131 form.$setSubmitted = function() {
20132 $animate.addClass(element, SUBMITTED_CLASS);
20133 form.$submitted = true;
20134 parentForm.$setSubmitted();
20135 };
1627620136 }
16277
1627820137
1627920138 /**
1628020139 * @ngdoc directive
1630920168 *
1631020169 * # Alias: {@link ng.directive:ngForm `ngForm`}
1631120170 *
16312 * In Angular forms can be nested. This means that the outer form is valid when all of the child
20171 * In Angular, forms can be nested. This means that the outer form is valid when all of the child
1631320172 * forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so
1631420173 * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to
1631520174 * `<form>` but can be nested. This allows you to have nested forms, which is very useful when
1632420183 * - `ng-invalid` is set if the form is invalid.
1632520184 * - `ng-pristine` is set if the form is pristine.
1632620185 * - `ng-dirty` is set if the form is dirty.
20186 * - `ng-submitted` is set if the form was submitted.
1632720187 *
1632820188 * Keep in mind that ngAnimate can detect each of these classes when added and removed.
1632920189 *
1635720217 * hitting enter in any of the input fields will trigger the click handler on the *first* button or
1635820218 * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`)
1635920219 *
16360 * @param {string=} name Name of the form. If specified, the form controller will be published into
16361 * related scope, under this name.
20220 * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is
20221 * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
20222 * to have access to the updated model.
1636220223 *
1636320224 * ## Animation Hooks
1636420225 *
1640620267 <form name="myForm" ng-controller="FormController" class="my-form">
1640720268 userType: <input name="input" ng-model="userType" required>
1640820269 <span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
16409 <tt>userType = {{userType}}</tt><br>
16410 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br>
16411 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br>
16412 <tt>myForm.$valid = {{myForm.$valid}}</tt><br>
16413 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
20270 <code>userType = {{userType}}</code><br>
20271 <code>myForm.input.$valid = {{myForm.input.$valid}}</code><br>
20272 <code>myForm.input.$error = {{myForm.input.$error}}</code><br>
20273 <code>myForm.$valid = {{myForm.$valid}}</code><br>
20274 <code>myForm.$error.required = {{!!myForm.$error.required}}</code><br>
1641420275 </form>
1641520276 </file>
1641620277 <file name="protractor.js" type="protractor">
1643620297 </file>
1643720298 </example>
1643820299 *
20300 * @param {string=} name Name of the form. If specified, the form controller will be published into
20301 * related scope, under this name.
1643920302 */
1644020303 var formDirectiveFactory = function(isNgForm) {
1644120304 return ['$timeout', function($timeout) {
1644320306 name: 'form',
1644420307 restrict: isNgForm ? 'EAC' : 'E',
1644520308 controller: FormController,
16446 compile: function() {
20309 compile: function ngFormCompile(formElement, attr) {
20310 // Setup initial state of the control
20311 formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS);
20312
20313 var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false);
20314
1644720315 return {
16448 pre: function(scope, formElement, attr, controller) {
16449 if (!attr.action) {
20316 pre: function ngFormPreLink(scope, formElement, attr, controller) {
20317 // if `action` attr is not present on the form, prevent the default action (submission)
20318 if (!('action' in attr)) {
1645020319 // we can't use jq events because if a form is destroyed during submission the default
1645120320 // action is not prevented. see #1238
1645220321 //
1645320322 // IE 9 is not affected because it doesn't fire a submit event and try to do a full
1645420323 // page reload if the form was destroyed by submission of the form via a click handler
1645520324 // on a button in the form. Looks like an IE9 specific bug.
16456 var preventDefaultListener = function(event) {
16457 event.preventDefault
16458 ? event.preventDefault()
16459 : event.returnValue = false; // IE
20325 var handleFormSubmission = function(event) {
20326 scope.$apply(function() {
20327 controller.$commitViewValue();
20328 controller.$setSubmitted();
20329 });
20330
20331 event.preventDefault();
1646020332 };
1646120333
16462 addEventListenerFn(formElement[0], 'submit', preventDefaultListener);
20334 addEventListenerFn(formElement[0], 'submit', handleFormSubmission);
1646320335
1646420336 // unregister the preventDefault listener so that we don't not leak memory but in a
1646520337 // way that will achieve the prevention of the default action.
1646620338 formElement.on('$destroy', function() {
1646720339 $timeout(function() {
16468 removeEventListenerFn(formElement[0], 'submit', preventDefaultListener);
20340 removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
1646920341 }, 0, false);
1647020342 });
1647120343 }
1647220344
16473 var parentFormCtrl = formElement.parent().controller('form'),
16474 alias = attr.name || attr.ngForm;
16475
16476 if (alias) {
16477 setter(scope, alias, controller, alias);
16478 }
16479 if (parentFormCtrl) {
16480 formElement.on('$destroy', function() {
16481 parentFormCtrl.$removeControl(controller);
16482 if (alias) {
16483 setter(scope, alias, undefined, alias);
16484 }
16485 extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards
20345 var parentFormCtrl = controller.$$parentForm;
20346
20347 if (nameAttr) {
20348 setter(scope, controller.$name, controller, controller.$name);
20349 attr.$observe(nameAttr, function(newValue) {
20350 if (controller.$name === newValue) return;
20351 setter(scope, controller.$name, undefined, controller.$name);
20352 parentFormCtrl.$$renameControl(controller, newValue);
20353 setter(scope, controller.$name, controller, controller.$name);
1648620354 });
1648720355 }
20356 formElement.on('$destroy', function() {
20357 parentFormCtrl.$removeControl(controller);
20358 if (nameAttr) {
20359 setter(scope, attr[nameAttr], undefined, controller.$name);
20360 }
20361 extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards
20362 });
1648820363 }
1648920364 };
1649020365 }
1649720372 var formDirective = formDirectiveFactory();
1649820373 var ngFormDirective = formDirectiveFactory(true);
1649920374
16500 /* global VALID_CLASS: true,
16501 INVALID_CLASS: true,
16502 PRISTINE_CLASS: true,
16503 DIRTY_CLASS: true
20375 /* global VALID_CLASS: false,
20376 INVALID_CLASS: false,
20377 PRISTINE_CLASS: false,
20378 DIRTY_CLASS: false,
20379 UNTOUCHED_CLASS: false,
20380 TOUCHED_CLASS: false,
20381 $ngModelMinErr: false,
1650420382 */
1650520383
20384 // Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
20385 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)/;
1650620386 var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
1650720387 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;
16508 var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
20388 var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
20389 var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
20390 var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
20391 var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
20392 var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
20393 var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
1650920394
1651020395 var inputType = {
1651120396
1651620401 * @description
1651720402 * Standard HTML text input with angular data binding, inherited by most of the `input` elements.
1651820403 *
16519 * *NOTE* Not every feature offered is available for all input types.
1652020404 *
1652120405 * @param {string} ngModel Assignable angular expression to data-bind to.
1652220406 * @param {string=} name Property name of the form under which the control is published.
1652720411 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
1652820412 * minlength.
1652920413 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
16530 * maxlength.
16531 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
16532 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
16533 * patterns defined as scope expressions.
20414 * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
20415 * any length.
20416 * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
20417 * that contains the regular expression body that will be converted to a regular expression
20418 * as in the ngPattern directive.
20419 * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
20420 * a RegExp found by evaluating the Angular expression given in the attribute value.
20421 * If the expression evaluates to a RegExp object, then this is used directly.
20422 * If the expression evaluates to a string, then it will be converted to a RegExp
20423 * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
20424 * `new RegExp('^abc$')`.<br />
20425 * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
20426 * start at the index of the last search's match, thus not taking the whole input value into
20427 * account.
1653420428 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1653520429 * interaction with the input element.
1653620430 * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
1654320437 <script>
1654420438 angular.module('textInputExample', [])
1654520439 .controller('ExampleController', ['$scope', function($scope) {
16546 $scope.text = 'guest';
16547 $scope.word = /^\s*\w*\s*$/;
20440 $scope.example = {
20441 text: 'guest',
20442 word: /^\s*\w*\s*$/
20443 };
1654820444 }]);
1654920445 </script>
1655020446 <form name="myForm" ng-controller="ExampleController">
16551 Single word: <input type="text" name="input" ng-model="text"
16552 ng-pattern="word" required ng-trim="false">
16553 <span class="error" ng-show="myForm.input.$error.required">
16554 Required!</span>
16555 <span class="error" ng-show="myForm.input.$error.pattern">
16556 Single word only!</span>
16557
16558 <tt>text = {{text}}</tt><br/>
20447 <label>Single word:
20448 <input type="text" name="input" ng-model="example.text"
20449 ng-pattern="example.word" required ng-trim="false">
20450 </label>
20451 <div role="alert">
20452 <span class="error" ng-show="myForm.input.$error.required">
20453 Required!</span>
20454 <span class="error" ng-show="myForm.input.$error.pattern">
20455 Single word only!</span>
20456 </div>
20457 <tt>text = {{example.text}}</tt><br/>
1655920458 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
1656020459 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
1656120460 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
1656320462 </form>
1656420463 </file>
1656520464 <file name="protractor.js" type="protractor">
16566 var text = element(by.binding('text'));
20465 var text = element(by.binding('example.text'));
1656720466 var valid = element(by.binding('myForm.input.$valid'));
16568 var input = element(by.model('text'));
20467 var input = element(by.model('example.text'));
1656920468
1657020469 it('should initialize to model', function() {
1657120470 expect(text.getText()).toContain('guest');
1659120490 */
1659220491 'text': textInputType,
1659320492
20493 /**
20494 * @ngdoc input
20495 * @name input[date]
20496 *
20497 * @description
20498 * Input with date validation and transformation. In browsers that do not yet support
20499 * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601
20500 * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many
20501 * modern browsers do not yet support this input type, it is important to provide cues to users on the
20502 * expected input format via a placeholder or label.
20503 *
20504 * The model must always be a Date object, otherwise Angular will throw an error.
20505 * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
20506 *
20507 * The timezone to be used to read/write the `Date` instance in the model can be defined using
20508 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
20509 *
20510 * @param {string} ngModel Assignable angular expression to data-bind to.
20511 * @param {string=} name Property name of the form under which the control is published.
20512 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
20513 * valid ISO date string (yyyy-MM-dd).
20514 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
20515 * a valid ISO date string (yyyy-MM-dd).
20516 * @param {string=} required Sets `required` validation error key if the value is not entered.
20517 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
20518 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
20519 * `required` when you want to data-bind to the `required` attribute.
20520 * @param {string=} ngChange Angular expression to be executed when input changes due to user
20521 * interaction with the input element.
20522 *
20523 * @example
20524 <example name="date-input-directive" module="dateInputExample">
20525 <file name="index.html">
20526 <script>
20527 angular.module('dateInputExample', [])
20528 .controller('DateController', ['$scope', function($scope) {
20529 $scope.example = {
20530 value: new Date(2013, 9, 22)
20531 };
20532 }]);
20533 </script>
20534 <form name="myForm" ng-controller="DateController as dateCtrl">
20535 <label for="exampleInput">Pick a date in 2013:</label>
20536 <input type="date" id="exampleInput" name="input" ng-model="example.value"
20537 placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required />
20538 <div role="alert">
20539 <span class="error" ng-show="myForm.input.$error.required">
20540 Required!</span>
20541 <span class="error" ng-show="myForm.input.$error.date">
20542 Not a valid date!</span>
20543 </div>
20544 <tt>value = {{example.value | date: "yyyy-MM-dd"}}</tt><br/>
20545 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
20546 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
20547 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
20548 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
20549 </form>
20550 </file>
20551 <file name="protractor.js" type="protractor">
20552 var value = element(by.binding('example.value | date: "yyyy-MM-dd"'));
20553 var valid = element(by.binding('myForm.input.$valid'));
20554 var input = element(by.model('example.value'));
20555
20556 // currently protractor/webdriver does not support
20557 // sending keys to all known HTML5 input controls
20558 // for various browsers (see https://github.com/angular/protractor/issues/562).
20559 function setInput(val) {
20560 // set the value of the element and force validation.
20561 var scr = "var ipt = document.getElementById('exampleInput'); " +
20562 "ipt.value = '" + val + "';" +
20563 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
20564 browser.executeScript(scr);
20565 }
20566
20567 it('should initialize to model', function() {
20568 expect(value.getText()).toContain('2013-10-22');
20569 expect(valid.getText()).toContain('myForm.input.$valid = true');
20570 });
20571
20572 it('should be invalid if empty', function() {
20573 setInput('');
20574 expect(value.getText()).toEqual('value =');
20575 expect(valid.getText()).toContain('myForm.input.$valid = false');
20576 });
20577
20578 it('should be invalid if over max', function() {
20579 setInput('2015-01-01');
20580 expect(value.getText()).toContain('');
20581 expect(valid.getText()).toContain('myForm.input.$valid = false');
20582 });
20583 </file>
20584 </example>
20585 */
20586 'date': createDateInputType('date', DATE_REGEXP,
20587 createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']),
20588 'yyyy-MM-dd'),
20589
20590 /**
20591 * @ngdoc input
20592 * @name input[datetime-local]
20593 *
20594 * @description
20595 * Input with datetime validation and transformation. In browsers that do not yet support
20596 * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
20597 * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`.
20598 *
20599 * The model must always be a Date object, otherwise Angular will throw an error.
20600 * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
20601 *
20602 * The timezone to be used to read/write the `Date` instance in the model can be defined using
20603 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
20604 *
20605 * @param {string} ngModel Assignable angular expression to data-bind to.
20606 * @param {string=} name Property name of the form under which the control is published.
20607 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
20608 * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss).
20609 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
20610 * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss).
20611 * @param {string=} required Sets `required` validation error key if the value is not entered.
20612 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
20613 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
20614 * `required` when you want to data-bind to the `required` attribute.
20615 * @param {string=} ngChange Angular expression to be executed when input changes due to user
20616 * interaction with the input element.
20617 *
20618 * @example
20619 <example name="datetimelocal-input-directive" module="dateExample">
20620 <file name="index.html">
20621 <script>
20622 angular.module('dateExample', [])
20623 .controller('DateController', ['$scope', function($scope) {
20624 $scope.example = {
20625 value: new Date(2010, 11, 28, 14, 57)
20626 };
20627 }]);
20628 </script>
20629 <form name="myForm" ng-controller="DateController as dateCtrl">
20630 <label for="exampleInput">Pick a date between in 2013:</label>
20631 <input type="datetime-local" id="exampleInput" name="input" ng-model="example.value"
20632 placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required />
20633 <div role="alert">
20634 <span class="error" ng-show="myForm.input.$error.required">
20635 Required!</span>
20636 <span class="error" ng-show="myForm.input.$error.datetimelocal">
20637 Not a valid date!</span>
20638 </div>
20639 <tt>value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}</tt><br/>
20640 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
20641 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
20642 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
20643 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
20644 </form>
20645 </file>
20646 <file name="protractor.js" type="protractor">
20647 var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"'));
20648 var valid = element(by.binding('myForm.input.$valid'));
20649 var input = element(by.model('example.value'));
20650
20651 // currently protractor/webdriver does not support
20652 // sending keys to all known HTML5 input controls
20653 // for various browsers (https://github.com/angular/protractor/issues/562).
20654 function setInput(val) {
20655 // set the value of the element and force validation.
20656 var scr = "var ipt = document.getElementById('exampleInput'); " +
20657 "ipt.value = '" + val + "';" +
20658 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
20659 browser.executeScript(scr);
20660 }
20661
20662 it('should initialize to model', function() {
20663 expect(value.getText()).toContain('2010-12-28T14:57:00');
20664 expect(valid.getText()).toContain('myForm.input.$valid = true');
20665 });
20666
20667 it('should be invalid if empty', function() {
20668 setInput('');
20669 expect(value.getText()).toEqual('value =');
20670 expect(valid.getText()).toContain('myForm.input.$valid = false');
20671 });
20672
20673 it('should be invalid if over max', function() {
20674 setInput('2015-01-01T23:59:00');
20675 expect(value.getText()).toContain('');
20676 expect(valid.getText()).toContain('myForm.input.$valid = false');
20677 });
20678 </file>
20679 </example>
20680 */
20681 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP,
20682 createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']),
20683 'yyyy-MM-ddTHH:mm:ss.sss'),
20684
20685 /**
20686 * @ngdoc input
20687 * @name input[time]
20688 *
20689 * @description
20690 * Input with time validation and transformation. In browsers that do not yet support
20691 * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
20692 * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
20693 * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
20694 *
20695 * The model must always be a Date object, otherwise Angular will throw an error.
20696 * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
20697 *
20698 * The timezone to be used to read/write the `Date` instance in the model can be defined using
20699 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
20700 *
20701 * @param {string} ngModel Assignable angular expression to data-bind to.
20702 * @param {string=} name Property name of the form under which the control is published.
20703 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
20704 * valid ISO time format (HH:mm:ss).
20705 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a
20706 * valid ISO time format (HH:mm:ss).
20707 * @param {string=} required Sets `required` validation error key if the value is not entered.
20708 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
20709 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
20710 * `required` when you want to data-bind to the `required` attribute.
20711 * @param {string=} ngChange Angular expression to be executed when input changes due to user
20712 * interaction with the input element.
20713 *
20714 * @example
20715 <example name="time-input-directive" module="timeExample">
20716 <file name="index.html">
20717 <script>
20718 angular.module('timeExample', [])
20719 .controller('DateController', ['$scope', function($scope) {
20720 $scope.example = {
20721 value: new Date(1970, 0, 1, 14, 57, 0)
20722 };
20723 }]);
20724 </script>
20725 <form name="myForm" ng-controller="DateController as dateCtrl">
20726 <label for="exampleInput">Pick a between 8am and 5pm:</label>
20727 <input type="time" id="exampleInput" name="input" ng-model="example.value"
20728 placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
20729 <div role="alert">
20730 <span class="error" ng-show="myForm.input.$error.required">
20731 Required!</span>
20732 <span class="error" ng-show="myForm.input.$error.time">
20733 Not a valid date!</span>
20734 </div>
20735 <tt>value = {{example.value | date: "HH:mm:ss"}}</tt><br/>
20736 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
20737 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
20738 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
20739 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
20740 </form>
20741 </file>
20742 <file name="protractor.js" type="protractor">
20743 var value = element(by.binding('example.value | date: "HH:mm:ss"'));
20744 var valid = element(by.binding('myForm.input.$valid'));
20745 var input = element(by.model('example.value'));
20746
20747 // currently protractor/webdriver does not support
20748 // sending keys to all known HTML5 input controls
20749 // for various browsers (https://github.com/angular/protractor/issues/562).
20750 function setInput(val) {
20751 // set the value of the element and force validation.
20752 var scr = "var ipt = document.getElementById('exampleInput'); " +
20753 "ipt.value = '" + val + "';" +
20754 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
20755 browser.executeScript(scr);
20756 }
20757
20758 it('should initialize to model', function() {
20759 expect(value.getText()).toContain('14:57:00');
20760 expect(valid.getText()).toContain('myForm.input.$valid = true');
20761 });
20762
20763 it('should be invalid if empty', function() {
20764 setInput('');
20765 expect(value.getText()).toEqual('value =');
20766 expect(valid.getText()).toContain('myForm.input.$valid = false');
20767 });
20768
20769 it('should be invalid if over max', function() {
20770 setInput('23:59:00');
20771 expect(value.getText()).toContain('');
20772 expect(valid.getText()).toContain('myForm.input.$valid = false');
20773 });
20774 </file>
20775 </example>
20776 */
20777 'time': createDateInputType('time', TIME_REGEXP,
20778 createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']),
20779 'HH:mm:ss.sss'),
20780
20781 /**
20782 * @ngdoc input
20783 * @name input[week]
20784 *
20785 * @description
20786 * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support
20787 * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
20788 * week format (yyyy-W##), for example: `2013-W02`.
20789 *
20790 * The model must always be a Date object, otherwise Angular will throw an error.
20791 * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
20792 *
20793 * The timezone to be used to read/write the `Date` instance in the model can be defined using
20794 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
20795 *
20796 * @param {string} ngModel Assignable angular expression to data-bind to.
20797 * @param {string=} name Property name of the form under which the control is published.
20798 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
20799 * valid ISO week format (yyyy-W##).
20800 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
20801 * a valid ISO week format (yyyy-W##).
20802 * @param {string=} required Sets `required` validation error key if the value is not entered.
20803 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
20804 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
20805 * `required` when you want to data-bind to the `required` attribute.
20806 * @param {string=} ngChange Angular expression to be executed when input changes due to user
20807 * interaction with the input element.
20808 *
20809 * @example
20810 <example name="week-input-directive" module="weekExample">
20811 <file name="index.html">
20812 <script>
20813 angular.module('weekExample', [])
20814 .controller('DateController', ['$scope', function($scope) {
20815 $scope.example = {
20816 value: new Date(2013, 0, 3)
20817 };
20818 }]);
20819 </script>
20820 <form name="myForm" ng-controller="DateController as dateCtrl">
20821 <label>Pick a date between in 2013:
20822 <input id="exampleInput" type="week" name="input" ng-model="example.value"
20823 placeholder="YYYY-W##" min="2012-W32"
20824 max="2013-W52" required />
20825 </label>
20826 <div role="alert">
20827 <span class="error" ng-show="myForm.input.$error.required">
20828 Required!</span>
20829 <span class="error" ng-show="myForm.input.$error.week">
20830 Not a valid date!</span>
20831 </div>
20832 <tt>value = {{example.value | date: "yyyy-Www"}}</tt><br/>
20833 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
20834 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
20835 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
20836 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
20837 </form>
20838 </file>
20839 <file name="protractor.js" type="protractor">
20840 var value = element(by.binding('example.value | date: "yyyy-Www"'));
20841 var valid = element(by.binding('myForm.input.$valid'));
20842 var input = element(by.model('example.value'));
20843
20844 // currently protractor/webdriver does not support
20845 // sending keys to all known HTML5 input controls
20846 // for various browsers (https://github.com/angular/protractor/issues/562).
20847 function setInput(val) {
20848 // set the value of the element and force validation.
20849 var scr = "var ipt = document.getElementById('exampleInput'); " +
20850 "ipt.value = '" + val + "';" +
20851 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
20852 browser.executeScript(scr);
20853 }
20854
20855 it('should initialize to model', function() {
20856 expect(value.getText()).toContain('2013-W01');
20857 expect(valid.getText()).toContain('myForm.input.$valid = true');
20858 });
20859
20860 it('should be invalid if empty', function() {
20861 setInput('');
20862 expect(value.getText()).toEqual('value =');
20863 expect(valid.getText()).toContain('myForm.input.$valid = false');
20864 });
20865
20866 it('should be invalid if over max', function() {
20867 setInput('2015-W01');
20868 expect(value.getText()).toContain('');
20869 expect(valid.getText()).toContain('myForm.input.$valid = false');
20870 });
20871 </file>
20872 </example>
20873 */
20874 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'),
20875
20876 /**
20877 * @ngdoc input
20878 * @name input[month]
20879 *
20880 * @description
20881 * Input with month validation and transformation. In browsers that do not yet support
20882 * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
20883 * month format (yyyy-MM), for example: `2009-01`.
20884 *
20885 * The model must always be a Date object, otherwise Angular will throw an error.
20886 * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
20887 * If the model is not set to the first of the month, the next view to model update will set it
20888 * to the first of the month.
20889 *
20890 * The timezone to be used to read/write the `Date` instance in the model can be defined using
20891 * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
20892 *
20893 * @param {string} ngModel Assignable angular expression to data-bind to.
20894 * @param {string=} name Property name of the form under which the control is published.
20895 * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be
20896 * a valid ISO month format (yyyy-MM).
20897 * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must
20898 * be a valid ISO month format (yyyy-MM).
20899 * @param {string=} required Sets `required` validation error key if the value is not entered.
20900 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
20901 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
20902 * `required` when you want to data-bind to the `required` attribute.
20903 * @param {string=} ngChange Angular expression to be executed when input changes due to user
20904 * interaction with the input element.
20905 *
20906 * @example
20907 <example name="month-input-directive" module="monthExample">
20908 <file name="index.html">
20909 <script>
20910 angular.module('monthExample', [])
20911 .controller('DateController', ['$scope', function($scope) {
20912 $scope.example = {
20913 value: new Date(2013, 9, 1)
20914 };
20915 }]);
20916 </script>
20917 <form name="myForm" ng-controller="DateController as dateCtrl">
20918 <label for="exampleInput">Pick a month in 2013:</label>
20919 <input id="exampleInput" type="month" name="input" ng-model="example.value"
20920 placeholder="yyyy-MM" min="2013-01" max="2013-12" required />
20921 <div role="alert">
20922 <span class="error" ng-show="myForm.input.$error.required">
20923 Required!</span>
20924 <span class="error" ng-show="myForm.input.$error.month">
20925 Not a valid month!</span>
20926 </div>
20927 <tt>value = {{example.value | date: "yyyy-MM"}}</tt><br/>
20928 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
20929 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
20930 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
20931 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
20932 </form>
20933 </file>
20934 <file name="protractor.js" type="protractor">
20935 var value = element(by.binding('example.value | date: "yyyy-MM"'));
20936 var valid = element(by.binding('myForm.input.$valid'));
20937 var input = element(by.model('example.value'));
20938
20939 // currently protractor/webdriver does not support
20940 // sending keys to all known HTML5 input controls
20941 // for various browsers (https://github.com/angular/protractor/issues/562).
20942 function setInput(val) {
20943 // set the value of the element and force validation.
20944 var scr = "var ipt = document.getElementById('exampleInput'); " +
20945 "ipt.value = '" + val + "';" +
20946 "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
20947 browser.executeScript(scr);
20948 }
20949
20950 it('should initialize to model', function() {
20951 expect(value.getText()).toContain('2013-10');
20952 expect(valid.getText()).toContain('myForm.input.$valid = true');
20953 });
20954
20955 it('should be invalid if empty', function() {
20956 setInput('');
20957 expect(value.getText()).toEqual('value =');
20958 expect(valid.getText()).toContain('myForm.input.$valid = false');
20959 });
20960
20961 it('should be invalid if over max', function() {
20962 setInput('2015-01');
20963 expect(value.getText()).toContain('');
20964 expect(valid.getText()).toContain('myForm.input.$valid = false');
20965 });
20966 </file>
20967 </example>
20968 */
20969 'month': createDateInputType('month', MONTH_REGEXP,
20970 createDateParser(MONTH_REGEXP, ['yyyy', 'MM']),
20971 'yyyy-MM'),
1659420972
1659520973 /**
1659620974 * @ngdoc input
1659920977 * @description
1660020978 * Text input with number validation and transformation. Sets the `number` validation
1660120979 * error if not a valid number.
20980 *
20981 * <div class="alert alert-warning">
20982 * The model must always be of type `number` otherwise Angular will throw an error.
20983 * Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt}
20984 * error docs for more information and an example of how to convert your model if necessary.
20985 * </div>
20986 *
20987 * ## Issues with HTML5 constraint validation
20988 *
20989 * In browsers that follow the
20990 * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29),
20991 * `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}.
20992 * If a non-number is entered in the input, the browser will report the value as an empty string,
20993 * which means the view / model values in `ngModel` and subsequently the scope value
20994 * will also be an empty string.
20995 *
1660220996 *
1660320997 * @param {string} ngModel Assignable angular expression to data-bind to.
1660420998 * @param {string=} name Property name of the form under which the control is published.
1661121005 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
1661221006 * minlength.
1661321007 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
16614 * maxlength.
16615 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
16616 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
16617 * patterns defined as scope expressions.
21008 * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
21009 * any length.
21010 * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
21011 * that contains the regular expression body that will be converted to a regular expression
21012 * as in the ngPattern directive.
21013 * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
21014 * a RegExp found by evaluating the Angular expression given in the attribute value.
21015 * If the expression evaluates to a RegExp object, then this is used directly.
21016 * If the expression evaluates to a string, then it will be converted to a RegExp
21017 * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
21018 * `new RegExp('^abc$')`.<br />
21019 * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
21020 * start at the index of the last search's match, thus not taking the whole input value into
21021 * account.
1661821022 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1661921023 * interaction with the input element.
1662021024 *
1662421028 <script>
1662521029 angular.module('numberExample', [])
1662621030 .controller('ExampleController', ['$scope', function($scope) {
16627 $scope.value = 12;
21031 $scope.example = {
21032 value: 12
21033 };
1662821034 }]);
1662921035 </script>
1663021036 <form name="myForm" ng-controller="ExampleController">
16631 Number: <input type="number" name="input" ng-model="value"
16632 min="0" max="99" required>
16633 <span class="error" ng-show="myForm.input.$error.required">
16634 Required!</span>
16635 <span class="error" ng-show="myForm.input.$error.number">
16636 Not valid number!</span>
16637 <tt>value = {{value}}</tt><br/>
21037 <label>Number:
21038 <input type="number" name="input" ng-model="example.value"
21039 min="0" max="99" required>
21040 </label>
21041 <div role="alert">
21042 <span class="error" ng-show="myForm.input.$error.required">
21043 Required!</span>
21044 <span class="error" ng-show="myForm.input.$error.number">
21045 Not valid number!</span>
21046 </div>
21047 <tt>value = {{example.value}}</tt><br/>
1663821048 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
1663921049 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
1664021050 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
1664221052 </form>
1664321053 </file>
1664421054 <file name="protractor.js" type="protractor">
16645 var value = element(by.binding('value'));
21055 var value = element(by.binding('example.value'));
1664621056 var valid = element(by.binding('myForm.input.$valid'));
16647 var input = element(by.model('value'));
21057 var input = element(by.model('example.value'));
1664821058
1664921059 it('should initialize to model', function() {
1665021060 expect(value.getText()).toContain('12');
1667821088 * Text input with URL validation. Sets the `url` validation error key if the content is not a
1667921089 * valid URL.
1668021090 *
21091 * <div class="alert alert-warning">
21092 * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex
21093 * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify
21094 * the built-in validators (see the {@link guide/forms Forms guide})
21095 * </div>
21096 *
1668121097 * @param {string} ngModel Assignable angular expression to data-bind to.
1668221098 * @param {string=} name Property name of the form under which the control is published.
1668321099 * @param {string=} required Sets `required` validation error key if the value is not entered.
1668721103 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
1668821104 * minlength.
1668921105 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
16690 * maxlength.
16691 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
16692 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
16693 * patterns defined as scope expressions.
21106 * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
21107 * any length.
21108 * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
21109 * that contains the regular expression body that will be converted to a regular expression
21110 * as in the ngPattern directive.
21111 * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
21112 * a RegExp found by evaluating the Angular expression given in the attribute value.
21113 * If the expression evaluates to a RegExp object, then this is used directly.
21114 * If the expression evaluates to a string, then it will be converted to a RegExp
21115 * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
21116 * `new RegExp('^abc$')`.<br />
21117 * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
21118 * start at the index of the last search's match, thus not taking the whole input value into
21119 * account.
1669421120 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1669521121 * interaction with the input element.
1669621122 *
1670021126 <script>
1670121127 angular.module('urlExample', [])
1670221128 .controller('ExampleController', ['$scope', function($scope) {
16703 $scope.text = 'http://google.com';
21129 $scope.url = {
21130 text: 'http://google.com'
21131 };
1670421132 }]);
1670521133 </script>
1670621134 <form name="myForm" ng-controller="ExampleController">
16707 URL: <input type="url" name="input" ng-model="text" required>
16708 <span class="error" ng-show="myForm.input.$error.required">
16709 Required!</span>
16710 <span class="error" ng-show="myForm.input.$error.url">
16711 Not valid url!</span>
16712 <tt>text = {{text}}</tt><br/>
21135 <label>URL:
21136 <input type="url" name="input" ng-model="url.text" required>
21137 <label>
21138 <div role="alert">
21139 <span class="error" ng-show="myForm.input.$error.required">
21140 Required!</span>
21141 <span class="error" ng-show="myForm.input.$error.url">
21142 Not valid url!</span>
21143 </div>
21144 <tt>text = {{url.text}}</tt><br/>
1671321145 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
1671421146 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
1671521147 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
1671821150 </form>
1671921151 </file>
1672021152 <file name="protractor.js" type="protractor">
16721 var text = element(by.binding('text'));
21153 var text = element(by.binding('url.text'));
1672221154 var valid = element(by.binding('myForm.input.$valid'));
16723 var input = element(by.model('text'));
21155 var input = element(by.model('url.text'));
1672421156
1672521157 it('should initialize to model', function() {
1672621158 expect(text.getText()).toContain('http://google.com');
1675421186 * @description
1675521187 * Text input with email validation. Sets the `email` validation error key if not a valid email
1675621188 * address.
21189 *
21190 * <div class="alert alert-warning">
21191 * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex
21192 * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can
21193 * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide})
21194 * </div>
1675721195 *
1675821196 * @param {string} ngModel Assignable angular expression to data-bind to.
1675921197 * @param {string=} name Property name of the form under which the control is published.
1676421202 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
1676521203 * minlength.
1676621204 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
16767 * maxlength.
16768 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
16769 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
16770 * patterns defined as scope expressions.
21205 * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
21206 * any length.
21207 * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
21208 * that contains the regular expression body that will be converted to a regular expression
21209 * as in the ngPattern directive.
21210 * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
21211 * a RegExp found by evaluating the Angular expression given in the attribute value.
21212 * If the expression evaluates to a RegExp object, then this is used directly.
21213 * If the expression evaluates to a string, then it will be converted to a RegExp
21214 * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
21215 * `new RegExp('^abc$')`.<br />
21216 * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
21217 * start at the index of the last search's match, thus not taking the whole input value into
21218 * account.
1677121219 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1677221220 * interaction with the input element.
1677321221 *
1677721225 <script>
1677821226 angular.module('emailExample', [])
1677921227 .controller('ExampleController', ['$scope', function($scope) {
16780 $scope.text = '[email protected]';
21228 $scope.email = {
21229 text: '[email protected]'
21230 };
1678121231 }]);
1678221232 </script>
1678321233 <form name="myForm" ng-controller="ExampleController">
16784 Email: <input type="email" name="input" ng-model="text" required>
16785 <span class="error" ng-show="myForm.input.$error.required">
16786 Required!</span>
16787 <span class="error" ng-show="myForm.input.$error.email">
16788 Not valid email!</span>
16789 <tt>text = {{text}}</tt><br/>
21234 <label>Email:
21235 <input type="email" name="input" ng-model="email.text" required>
21236 </label>
21237 <div role="alert">
21238 <span class="error" ng-show="myForm.input.$error.required">
21239 Required!</span>
21240 <span class="error" ng-show="myForm.input.$error.email">
21241 Not valid email!</span>
21242 </div>
21243 <tt>text = {{email.text}}</tt><br/>
1679021244 <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
1679121245 <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
1679221246 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
1679521249 </form>
1679621250 </file>
1679721251 <file name="protractor.js" type="protractor">
16798 var text = element(by.binding('text'));
21252 var text = element(by.binding('email.text'));
1679921253 var valid = element(by.binding('myForm.input.$valid'));
16800 var input = element(by.model('text'));
21254 var input = element(by.model('email.text'));
1680121255
1680221256 it('should initialize to model', function() {
1680321257 expect(text.getText()).toContain('[email protected]');
1683121285 * HTML radio button.
1683221286 *
1683321287 * @param {string} ngModel Assignable angular expression to data-bind to.
16834 * @param {string} value The value to which the expression should be set when selected.
21288 * @param {string} value The value to which the `ngModel` expression should be set when selected.
21289 * Note that `value` only supports `string` values, i.e. the scope model needs to be a string,
21290 * too. Use `ngValue` if you need complex models (`number`, `object`, ...).
1683521291 * @param {string=} name Property name of the form under which the control is published.
1683621292 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1683721293 * interaction with the input element.
16838 * @param {string} ngValue Angular expression which sets the value to which the expression should
16839 * be set when selected.
21294 * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio
21295 * is selected. Should be used instead of the `value` attribute if you need
21296 * a non-string `ngModel` (`boolean`, `array`, ...).
1684021297 *
1684121298 * @example
1684221299 <example name="radio-input-directive" module="radioExample">
1684421301 <script>
1684521302 angular.module('radioExample', [])
1684621303 .controller('ExampleController', ['$scope', function($scope) {
16847 $scope.color = 'blue';
21304 $scope.color = {
21305 name: 'blue'
21306 };
1684821307 $scope.specialValue = {
1684921308 "id": "12345",
1685021309 "value": "green"
1685221311 }]);
1685321312 </script>
1685421313 <form name="myForm" ng-controller="ExampleController">
16855 <input type="radio" ng-model="color" value="red"> Red <br/>
16856 <input type="radio" ng-model="color" ng-value="specialValue"> Green <br/>
16857 <input type="radio" ng-model="color" value="blue"> Blue <br/>
16858 <tt>color = {{color | json}}</tt><br/>
21314 <label>
21315 <input type="radio" ng-model="color.name" value="red">
21316 Red
21317 </label><br/>
21318 <label>
21319 <input type="radio" ng-model="color.name" ng-value="specialValue">
21320 Green
21321 </label><br/>
21322 <label>
21323 <input type="radio" ng-model="color.name" value="blue">
21324 Blue
21325 </label><br/>
21326 <tt>color = {{color.name | json}}</tt><br/>
1685921327 </form>
1686021328 Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
1686121329 </file>
1686221330 <file name="protractor.js" type="protractor">
1686321331 it('should change state', function() {
16864 var color = element(by.binding('color'));
21332 var color = element(by.binding('color.name'));
1686521333
1686621334 expect(color.getText()).toContain('blue');
1686721335
16868 element.all(by.model('color')).get(0).click();
21336 element.all(by.model('color.name')).get(0).click();
1686921337
1687021338 expect(color.getText()).toContain('red');
1687121339 });
1688421352 *
1688521353 * @param {string} ngModel Assignable angular expression to data-bind to.
1688621354 * @param {string=} name Property name of the form under which the control is published.
16887 * @param {string=} ngTrueValue The value to which the expression should be set when selected.
16888 * @param {string=} ngFalseValue The value to which the expression should be set when not selected.
21355 * @param {expression=} ngTrueValue The value to which the expression should be set when selected.
21356 * @param {expression=} ngFalseValue The value to which the expression should be set when not selected.
1688921357 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1689021358 * interaction with the input element.
1689121359 *
1689521363 <script>
1689621364 angular.module('checkboxExample', [])
1689721365 .controller('ExampleController', ['$scope', function($scope) {
16898 $scope.value1 = true;
16899 $scope.value2 = 'YES'
21366 $scope.checkboxModel = {
21367 value1 : true,
21368 value2 : 'YES'
21369 };
1690021370 }]);
1690121371 </script>
1690221372 <form name="myForm" ng-controller="ExampleController">
16903 Value1: <input type="checkbox" ng-model="value1"> <br/>
16904 Value2: <input type="checkbox" ng-model="value2"
16905 ng-true-value="YES" ng-false-value="NO"> <br/>
16906 <tt>value1 = {{value1}}</tt><br/>
16907 <tt>value2 = {{value2}}</tt><br/>
21373 <label>Value1:
21374 <input type="checkbox" ng-model="checkboxModel.value1">
21375 </label><br/>
21376 <label>Value2:
21377 <input type="checkbox" ng-model="checkboxModel.value2"
21378 ng-true-value="'YES'" ng-false-value="'NO'">
21379 </label><br/>
21380 <tt>value1 = {{checkboxModel.value1}}</tt><br/>
21381 <tt>value2 = {{checkboxModel.value2}}</tt><br/>
1690821382 </form>
1690921383 </file>
1691021384 <file name="protractor.js" type="protractor">
1691121385 it('should change state', function() {
16912 var value1 = element(by.binding('value1'));
16913 var value2 = element(by.binding('value2'));
21386 var value1 = element(by.binding('checkboxModel.value1'));
21387 var value2 = element(by.binding('checkboxModel.value2'));
1691421388
1691521389 expect(value1.getText()).toContain('true');
1691621390 expect(value2.getText()).toContain('YES');
1691721391
16918 element(by.model('value1')).click();
16919 element(by.model('value2')).click();
21392 element(by.model('checkboxModel.value1')).click();
21393 element(by.model('checkboxModel.value2')).click();
1692021394
1692121395 expect(value1.getText()).toContain('false');
1692221396 expect(value2.getText()).toContain('NO');
1693321407 'file': noop
1693421408 };
1693521409
16936 // A helper function to call $setValidity and return the value / undefined,
16937 // a pattern that is repeated a lot in the input validation logic.
16938 function validate(ctrl, validatorName, validity, value){
16939 ctrl.$setValidity(validatorName, validity);
16940 return validity ? value : undefined;
21410 function stringBasedInputType(ctrl) {
21411 ctrl.$formatters.push(function(value) {
21412 return ctrl.$isEmpty(value) ? value : value.toString();
21413 });
1694121414 }
1694221415
16943 function testFlags(validity, flags) {
16944 var i, flag;
16945 if (flags) {
16946 for (i=0; i<flags.length; ++i) {
16947 flag = flags[i];
16948 if (validity[flag]) {
16949 return true;
16950 }
16951 }
16952 }
16953 return false;
21416 function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
21417 baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
21418 stringBasedInputType(ctrl);
1695421419 }
1695521420
16956 // Pass validity so that behaviour can be mocked easier.
16957 function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) {
16958 if (isObject(validity)) {
16959 ctrl.$$hasNativeValidators = true;
16960 var validator = function(value) {
16961 // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can
16962 // perform the required validation)
16963 if (!ctrl.$error[validatorName] &&
16964 !testFlags(validity, ignoreFlags) &&
16965 testFlags(validity, badFlags)) {
16966 ctrl.$setValidity(validatorName, false);
16967 return;
16968 }
16969 return value;
16970 };
16971 ctrl.$parsers.push(validator);
16972 }
16973 }
16974
16975 function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
16976 var validity = element.prop(VALIDITY_STATE_PROPERTY);
16977 var placeholder = element[0].placeholder, noevent = {};
21421 function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
1697821422 var type = lowercase(element[0].type);
16979 ctrl.$$validityState = validity;
1698021423
1698121424 // In composition mode, users are still inputing intermediate text buffer,
1698221425 // hold the listener until composition is done.
1699521438 }
1699621439
1699721440 var listener = function(ev) {
21441 if (timeout) {
21442 $browser.defer.cancel(timeout);
21443 timeout = null;
21444 }
1699821445 if (composing) return;
16999 var value = element.val();
17000
17001 // IE (11 and under) seem to emit an 'input' event if the placeholder value changes.
17002 // We don't want to dirty the value when this happens, so we abort here. Unfortunately,
17003 // IE also sends input events for other non-input-related things, (such as focusing on a
17004 // form control), so this change is not entirely enough to solve this.
17005 if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) {
17006 placeholder = element[0].placeholder;
17007 return;
17008 }
21446 var value = element.val(),
21447 event = ev && ev.type;
1700921448
1701021449 // By default we will trim the value
1701121450 // If the attribute ng-trim exists we will avoid trimming
1701221451 // If input type is 'password', the value is never trimmed
17013 if (type !== 'password' && (toBoolean(attr.ngTrim || 'T'))) {
21452 if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) {
1701421453 value = trim(value);
1701521454 }
1701621455
17017 // If a control is suffering from bad input, browsers discard its value, so it may be
17018 // necessary to revalidate even if the control's value is the same empty value twice in
17019 // a row.
17020 var revalidate = validity && ctrl.$$hasNativeValidators;
17021 if (ctrl.$viewValue !== value || (value === '' && revalidate)) {
17022 if (scope.$$phase) {
17023 ctrl.$setViewValue(value);
17024 } else {
17025 scope.$apply(function() {
17026 ctrl.$setViewValue(value);
17027 });
17028 }
21456 // If a control is suffering from bad input (due to native validators), browsers discard its
21457 // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the
21458 // control's value is the same empty value twice in a row.
21459 if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) {
21460 ctrl.$setViewValue(value, event);
1702921461 }
1703021462 };
1703121463
1703621468 } else {
1703721469 var timeout;
1703821470
17039 var deferListener = function() {
21471 var deferListener = function(ev, input, origValue) {
1704021472 if (!timeout) {
1704121473 timeout = $browser.defer(function() {
17042 listener();
1704321474 timeout = null;
21475 if (!input || input.value !== origValue) {
21476 listener(ev);
21477 }
1704421478 });
1704521479 }
1704621480 };
1705221486 // command modifiers arrows
1705321487 if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
1705421488
17055 deferListener();
21489 deferListener(event, this, this.value);
1705621490 });
1705721491
1705821492 // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
1706821502 ctrl.$render = function() {
1706921503 element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
1707021504 };
17071
17072 // pattern validator
17073 var pattern = attr.ngPattern,
17074 patternValidator,
17075 match;
17076
17077 if (pattern) {
17078 var validateRegex = function(regexp, value) {
17079 return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value);
17080 };
17081 match = pattern.match(/^\/(.*)\/([gim]*)$/);
17082 if (match) {
17083 pattern = new RegExp(match[1], match[2]);
17084 patternValidator = function(value) {
17085 return validateRegex(pattern, value);
21505 }
21506
21507 function weekParser(isoWeek, existingDate) {
21508 if (isDate(isoWeek)) {
21509 return isoWeek;
21510 }
21511
21512 if (isString(isoWeek)) {
21513 WEEK_REGEXP.lastIndex = 0;
21514 var parts = WEEK_REGEXP.exec(isoWeek);
21515 if (parts) {
21516 var year = +parts[1],
21517 week = +parts[2],
21518 hours = 0,
21519 minutes = 0,
21520 seconds = 0,
21521 milliseconds = 0,
21522 firstThurs = getFirstThursdayOfYear(year),
21523 addDays = (week - 1) * 7;
21524
21525 if (existingDate) {
21526 hours = existingDate.getHours();
21527 minutes = existingDate.getMinutes();
21528 seconds = existingDate.getSeconds();
21529 milliseconds = existingDate.getMilliseconds();
21530 }
21531
21532 return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds);
21533 }
21534 }
21535
21536 return NaN;
21537 }
21538
21539 function createDateParser(regexp, mapping) {
21540 return function(iso, date) {
21541 var parts, map;
21542
21543 if (isDate(iso)) {
21544 return iso;
21545 }
21546
21547 if (isString(iso)) {
21548 // When a date is JSON'ified to wraps itself inside of an extra
21549 // set of double quotes. This makes the date parsing code unable
21550 // to match the date string and parse it as a date.
21551 if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') {
21552 iso = iso.substring(1, iso.length - 1);
21553 }
21554 if (ISO_DATE_REGEXP.test(iso)) {
21555 return new Date(iso);
21556 }
21557 regexp.lastIndex = 0;
21558 parts = regexp.exec(iso);
21559
21560 if (parts) {
21561 parts.shift();
21562 if (date) {
21563 map = {
21564 yyyy: date.getFullYear(),
21565 MM: date.getMonth() + 1,
21566 dd: date.getDate(),
21567 HH: date.getHours(),
21568 mm: date.getMinutes(),
21569 ss: date.getSeconds(),
21570 sss: date.getMilliseconds() / 1000
21571 };
21572 } else {
21573 map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 };
21574 }
21575
21576 forEach(parts, function(part, index) {
21577 if (index < mapping.length) {
21578 map[mapping[index]] = +part;
21579 }
21580 });
21581 return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
21582 }
21583 }
21584
21585 return NaN;
21586 };
21587 }
21588
21589 function createDateInputType(type, regexp, parseDate, format) {
21590 return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
21591 badInputChecker(scope, element, attr, ctrl);
21592 baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
21593 var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
21594 var previousDate;
21595
21596 ctrl.$$parserName = type;
21597 ctrl.$parsers.push(function(value) {
21598 if (ctrl.$isEmpty(value)) return null;
21599 if (regexp.test(value)) {
21600 // Note: We cannot read ctrl.$modelValue, as there might be a different
21601 // parser/formatter in the processing chain so that the model
21602 // contains some different data format!
21603 var parsedDate = parseDate(value, previousDate);
21604 if (timezone) {
21605 parsedDate = convertTimezoneToLocal(parsedDate, timezone);
21606 }
21607 return parsedDate;
21608 }
21609 return undefined;
21610 });
21611
21612 ctrl.$formatters.push(function(value) {
21613 if (value && !isDate(value)) {
21614 throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value);
21615 }
21616 if (isValidDate(value)) {
21617 previousDate = value;
21618 if (previousDate && timezone) {
21619 previousDate = convertTimezoneToLocal(previousDate, timezone, true);
21620 }
21621 return $filter('date')(value, format, timezone);
21622 } else {
21623 previousDate = null;
21624 return '';
21625 }
21626 });
21627
21628 if (isDefined(attr.min) || attr.ngMin) {
21629 var minVal;
21630 ctrl.$validators.min = function(value) {
21631 return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal;
1708621632 };
17087 } else {
17088 patternValidator = function(value) {
17089 var patternObj = scope.$eval(pattern);
17090
17091 if (!patternObj || !patternObj.test) {
17092 throw minErr('ngPattern')('noregexp',
17093 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern,
17094 patternObj, startingTag(element));
17095 }
17096 return validateRegex(patternObj, value);
21633 attr.$observe('min', function(val) {
21634 minVal = parseObservedDateValue(val);
21635 ctrl.$validate();
21636 });
21637 }
21638
21639 if (isDefined(attr.max) || attr.ngMax) {
21640 var maxVal;
21641 ctrl.$validators.max = function(value) {
21642 return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
1709721643 };
17098 }
17099
17100 ctrl.$formatters.push(patternValidator);
17101 ctrl.$parsers.push(patternValidator);
17102 }
17103
17104 // min length validator
17105 if (attr.ngMinlength) {
17106 var minlength = int(attr.ngMinlength);
17107 var minLengthValidator = function(value) {
17108 return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value);
17109 };
17110
17111 ctrl.$parsers.push(minLengthValidator);
17112 ctrl.$formatters.push(minLengthValidator);
17113 }
17114
17115 // max length validator
17116 if (attr.ngMaxlength) {
17117 var maxlength = int(attr.ngMaxlength);
17118 var maxLengthValidator = function(value) {
17119 return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value);
17120 };
17121
17122 ctrl.$parsers.push(maxLengthValidator);
17123 ctrl.$formatters.push(maxLengthValidator);
21644 attr.$observe('max', function(val) {
21645 maxVal = parseObservedDateValue(val);
21646 ctrl.$validate();
21647 });
21648 }
21649
21650 function isValidDate(value) {
21651 // Invalid Date: getTime() returns NaN
21652 return value && !(value.getTime && value.getTime() !== value.getTime());
21653 }
21654
21655 function parseObservedDateValue(val) {
21656 return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined;
21657 }
21658 };
21659 }
21660
21661 function badInputChecker(scope, element, attr, ctrl) {
21662 var node = element[0];
21663 var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
21664 if (nativeValidation) {
21665 ctrl.$parsers.push(function(value) {
21666 var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
21667 // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430):
21668 // - also sets validity.badInput (should only be validity.typeMismatch).
21669 // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email)
21670 // - can ignore this case as we can still read out the erroneous email...
21671 return validity.badInput && !validity.typeMismatch ? undefined : value;
21672 });
1712421673 }
1712521674 }
1712621675
17127 var numberBadFlags = ['badInput'];
17128
1712921676 function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
17130 textInputType(scope, element, attr, ctrl, $sniffer, $browser);
17131
21677 badInputChecker(scope, element, attr, ctrl);
21678 baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
21679
21680 ctrl.$$parserName = 'number';
1713221681 ctrl.$parsers.push(function(value) {
17133 var empty = ctrl.$isEmpty(value);
17134 if (empty || NUMBER_REGEXP.test(value)) {
17135 ctrl.$setValidity('number', true);
17136 return value === '' ? null : (empty ? value : parseFloat(value));
17137 } else {
17138 ctrl.$setValidity('number', false);
17139 return undefined;
17140 }
21682 if (ctrl.$isEmpty(value)) return null;
21683 if (NUMBER_REGEXP.test(value)) return parseFloat(value);
21684 return undefined;
1714121685 });
1714221686
17143 addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);
17144
1714521687 ctrl.$formatters.push(function(value) {
17146 return ctrl.$isEmpty(value) ? '' : '' + value;
21688 if (!ctrl.$isEmpty(value)) {
21689 if (!isNumber(value)) {
21690 throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value);
21691 }
21692 value = value.toString();
21693 }
21694 return value;
1714721695 });
1714821696
17149 if (attr.min) {
17150 var minValidator = function(value) {
17151 var min = parseFloat(attr.min);
17152 return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value);
21697 if (isDefined(attr.min) || attr.ngMin) {
21698 var minVal;
21699 ctrl.$validators.min = function(value) {
21700 return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
1715321701 };
1715421702
17155 ctrl.$parsers.push(minValidator);
17156 ctrl.$formatters.push(minValidator);
21703 attr.$observe('min', function(val) {
21704 if (isDefined(val) && !isNumber(val)) {
21705 val = parseFloat(val, 10);
21706 }
21707 minVal = isNumber(val) && !isNaN(val) ? val : undefined;
21708 // TODO(matsko): implement validateLater to reduce number of validations
21709 ctrl.$validate();
21710 });
1715721711 }
1715821712
17159 if (attr.max) {
17160 var maxValidator = function(value) {
17161 var max = parseFloat(attr.max);
17162 return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value);
21713 if (isDefined(attr.max) || attr.ngMax) {
21714 var maxVal;
21715 ctrl.$validators.max = function(value) {
21716 return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
1716321717 };
1716421718
17165 ctrl.$parsers.push(maxValidator);
17166 ctrl.$formatters.push(maxValidator);
21719 attr.$observe('max', function(val) {
21720 if (isDefined(val) && !isNumber(val)) {
21721 val = parseFloat(val, 10);
21722 }
21723 maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
21724 // TODO(matsko): implement validateLater to reduce number of validations
21725 ctrl.$validate();
21726 });
1716721727 }
17168
17169 ctrl.$formatters.push(function(value) {
17170 return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value);
17171 });
1717221728 }
1717321729
1717421730 function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
17175 textInputType(scope, element, attr, ctrl, $sniffer, $browser);
17176
17177 var urlValidator = function(value) {
17178 return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value);
21731 // Note: no badInputChecker here by purpose as `url` is only a validation
21732 // in browsers, i.e. we can always read out input.value even if it is not valid!
21733 baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
21734 stringBasedInputType(ctrl);
21735
21736 ctrl.$$parserName = 'url';
21737 ctrl.$validators.url = function(modelValue, viewValue) {
21738 var value = modelValue || viewValue;
21739 return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
1717921740 };
17180
17181 ctrl.$formatters.push(urlValidator);
17182 ctrl.$parsers.push(urlValidator);
1718321741 }
1718421742
1718521743 function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
17186 textInputType(scope, element, attr, ctrl, $sniffer, $browser);
17187
17188 var emailValidator = function(value) {
17189 return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value);
21744 // Note: no badInputChecker here by purpose as `url` is only a validation
21745 // in browsers, i.e. we can always read out input.value even if it is not valid!
21746 baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
21747 stringBasedInputType(ctrl);
21748
21749 ctrl.$$parserName = 'email';
21750 ctrl.$validators.email = function(modelValue, viewValue) {
21751 var value = modelValue || viewValue;
21752 return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
1719021753 };
17191
17192 ctrl.$formatters.push(emailValidator);
17193 ctrl.$parsers.push(emailValidator);
1719421754 }
1719521755
1719621756 function radioInputType(scope, element, attr, ctrl) {
1719921759 element.attr('name', nextUid());
1720021760 }
1720121761
17202 element.on('click', function() {
21762 var listener = function(ev) {
1720321763 if (element[0].checked) {
17204 scope.$apply(function() {
17205 ctrl.$setViewValue(attr.value);
17206 });
17207 }
17208 });
21764 ctrl.$setViewValue(attr.value, ev && ev.type);
21765 }
21766 };
21767
21768 element.on('click', listener);
1720921769
1721021770 ctrl.$render = function() {
1721121771 var value = attr.value;
1721521775 attr.$observe('value', ctrl.$render);
1721621776 }
1721721777
17218 function checkboxInputType(scope, element, attr, ctrl) {
17219 var trueValue = attr.ngTrueValue,
17220 falseValue = attr.ngFalseValue;
17221
17222 if (!isString(trueValue)) trueValue = true;
17223 if (!isString(falseValue)) falseValue = false;
17224
17225 element.on('click', function() {
17226 scope.$apply(function() {
17227 ctrl.$setViewValue(element[0].checked);
17228 });
17229 });
21778 function parseConstantExpr($parse, context, name, expression, fallback) {
21779 var parseFn;
21780 if (isDefined(expression)) {
21781 parseFn = $parse(expression);
21782 if (!parseFn.constant) {
21783 throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' +
21784 '`{1}`.', name, expression);
21785 }
21786 return parseFn(context);
21787 }
21788 return fallback;
21789 }
21790
21791 function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
21792 var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true);
21793 var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false);
21794
21795 var listener = function(ev) {
21796 ctrl.$setViewValue(element[0].checked, ev && ev.type);
21797 };
21798
21799 element.on('click', listener);
1723021800
1723121801 ctrl.$render = function() {
1723221802 element[0].checked = ctrl.$viewValue;
1723321803 };
1723421804
17235 // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox.
21805 // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false`
21806 // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert
21807 // it to a boolean.
1723621808 ctrl.$isEmpty = function(value) {
17237 return value !== trueValue;
21809 return value === false;
1723821810 };
1723921811
1724021812 ctrl.$formatters.push(function(value) {
17241 return value === trueValue;
21813 return equals(value, trueValue);
1724221814 });
1724321815
1724421816 ctrl.$parsers.push(function(value) {
1726621838 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
1726721839 * minlength.
1726821840 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
17269 * maxlength.
17270 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
17271 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
17272 * patterns defined as scope expressions.
21841 * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
21842 * length.
21843 * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
21844 * a RegExp found by evaluating the Angular expression given in the attribute value.
21845 * If the expression evaluates to a RegExp object, then this is used directly.
21846 * If the expression evaluates to a string, then it will be converted to a RegExp
21847 * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
21848 * `new RegExp('^abc$')`.<br />
21849 * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
21850 * start at the index of the last search's match, thus not taking the whole input value into
21851 * account.
1727321852 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1727421853 * interaction with the input element.
1727521854 * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
1728221861 * @restrict E
1728321862 *
1728421863 * @description
17285 * HTML input element control with angular data-binding. Input control follows HTML5 input types
17286 * and polyfills the HTML5 validation behavior for older browsers.
17287 *
17288 * *NOTE* Not every feature offered is available for all input types.
21864 * HTML input element control. When used together with {@link ngModel `ngModel`}, it provides data-binding,
21865 * input state control, and validation.
21866 * Input control follows HTML5 input types and polyfills the HTML5 validation behavior for older browsers.
21867 *
21868 * <div class="alert alert-warning">
21869 * **Note:** Not every feature offered is available for all input types.
21870 * Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`.
21871 * </div>
1728921872 *
1729021873 * @param {string} ngModel Assignable angular expression to data-bind to.
1729121874 * @param {string=} name Property name of the form under which the control is published.
1729421877 * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
1729521878 * minlength.
1729621879 * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
17297 * maxlength.
17298 * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
17299 * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
17300 * patterns defined as scope expressions.
21880 * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
21881 * length.
21882 * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
21883 * a RegExp found by evaluating the Angular expression given in the attribute value.
21884 * If the expression evaluates to a RegExp object, then this is used directly.
21885 * If the expression evaluates to a string, then it will be converted to a RegExp
21886 * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
21887 * `new RegExp('^abc$')`.<br />
21888 * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
21889 * start at the index of the last search's match, thus not taking the whole input value into
21890 * account.
1730121891 * @param {string=} ngChange Angular expression to be executed when input changes due to user
1730221892 * interaction with the input element.
1730321893 * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
1731521905 </script>
1731621906 <div ng-controller="ExampleController">
1731721907 <form name="myForm">
17318 User name: <input type="text" name="userName" ng-model="user.name" required>
17319 <span class="error" ng-show="myForm.userName.$error.required">
17320 Required!</span><br>
17321 Last name: <input type="text" name="lastName" ng-model="user.last"
17322 ng-minlength="3" ng-maxlength="10">
17323 <span class="error" ng-show="myForm.lastName.$error.minlength">
17324 Too short!</span>
17325 <span class="error" ng-show="myForm.lastName.$error.maxlength">
17326 Too long!</span><br>
21908 <label>
21909 User name:
21910 <input type="text" name="userName" ng-model="user.name" required>
21911 </label>
21912 <div role="alert">
21913 <span class="error" ng-show="myForm.userName.$error.required">
21914 Required!</span>
21915 </div>
21916 <label>
21917 Last name:
21918 <input type="text" name="lastName" ng-model="user.last"
21919 ng-minlength="3" ng-maxlength="10">
21920 </label>
21921 <div role="alert">
21922 <span class="error" ng-show="myForm.lastName.$error.minlength">
21923 Too short!</span>
21924 <span class="error" ng-show="myForm.lastName.$error.maxlength">
21925 Too long!</span>
21926 </div>
1732721927 </form>
1732821928 <hr>
1732921929 <tt>user = {{user}}</tt><br/>
17330 <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br>
17331 <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br>
17332 <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br>
17333 <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br>
17334 <tt>myForm.$valid = {{myForm.$valid}}</tt><br>
17335 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
17336 <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br>
17337 <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br>
21930 <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br/>
21931 <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br/>
21932 <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br/>
21933 <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br/>
21934 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
21935 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
21936 <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br/>
21937 <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br/>
1733821938 </div>
1733921939 </file>
1734021940 <file name="protractor.js" type="protractor">
17341 var user = element(by.binding('{{user}}'));
21941 var user = element(by.exactBinding('user'));
1734221942 var userNameValid = element(by.binding('myForm.userName.$valid'));
1734321943 var lastNameValid = element(by.binding('myForm.lastName.$valid'));
1734421944 var lastNameError = element(by.binding('myForm.lastName.$error'));
1739221992 </file>
1739321993 </example>
1739421994 */
17395 var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
21995 var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
21996 function($browser, $sniffer, $filter, $parse) {
1739621997 return {
1739721998 restrict: 'E',
17398 require: '?ngModel',
17399 link: function(scope, element, attr, ctrl) {
17400 if (ctrl) {
17401 (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
17402 $browser);
21999 require: ['?ngModel'],
22000 link: {
22001 pre: function(scope, element, attr, ctrls) {
22002 if (ctrls[0]) {
22003 (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
22004 $browser, $filter, $parse);
22005 }
1740322006 }
1740422007 }
1740522008 };
1740622009 }];
1740722010
17408 var VALID_CLASS = 'ng-valid',
17409 INVALID_CLASS = 'ng-invalid',
17410 PRISTINE_CLASS = 'ng-pristine',
17411 DIRTY_CLASS = 'ng-dirty';
17412
17413 /**
17414 * @ngdoc type
17415 * @name ngModel.NgModelController
17416 *
17417 * @property {string} $viewValue Actual string value in the view.
17418 * @property {*} $modelValue The value in the model, that the control is bound to.
17419 * @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
17420 the control reads value from the DOM. Each function is called, in turn, passing the value
17421 through to the next. The last return value is used to populate the model.
17422 Used to sanitize / convert the value as well as validation. For validation,
17423 the parsers should update the validity state using
17424 {@link ngModel.NgModelController#$setValidity $setValidity()},
17425 and return `undefined` for invalid values.
17426
17427 *
17428 * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever
17429 the model value changes. Each function is called, in turn, passing the value through to the
17430 next. Used to format / convert values for display in the control and validation.
17431 * ```js
17432 * function formatter(value) {
17433 * if (value) {
17434 * return value.toUpperCase();
17435 * }
17436 * }
17437 * ngModel.$formatters.push(formatter);
17438 * ```
17439 *
17440 * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
17441 * view value has changed. It is called with no arguments, and its return value is ignored.
17442 * This can be used in place of additional $watches against the model value.
17443 *
17444 * @property {Object} $error An object hash with all errors as keys.
17445 *
17446 * @property {boolean} $pristine True if user has not interacted with the control yet.
17447 * @property {boolean} $dirty True if user has already interacted with the control.
17448 * @property {boolean} $valid True if there is no error.
17449 * @property {boolean} $invalid True if at least one error on the control.
17450 *
17451 * @description
17452 *
17453 * `NgModelController` provides API for the `ng-model` directive. The controller contains
17454 * services for data-binding, validation, CSS updates, and value formatting and parsing. It
17455 * purposefully does not contain any logic which deals with DOM rendering or listening to
17456 * DOM events. Such DOM related logic should be provided by other directives which make use of
17457 * `NgModelController` for data-binding.
17458 *
17459 * ## Custom Control Example
17460 * This example shows how to use `NgModelController` with a custom control to achieve
17461 * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
17462 * collaborate together to achieve the desired result.
17463 *
17464 * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element
17465 * contents be edited in place by the user. This will not work on older browsers.
17466 *
17467 * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize}
17468 * module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`).
17469 * However, as we are using `$sce` the model can still decide to to provide unsafe content if it marks
17470 * that content using the `$sce` service.
17471 *
17472 * <example name="NgModelController" module="customControl" deps="angular-sanitize.js">
17473 <file name="style.css">
17474 [contenteditable] {
17475 border: 1px solid black;
17476 background-color: white;
17477 min-height: 20px;
17478 }
17479
17480 .ng-invalid {
17481 border: 1px solid red;
17482 }
17483
17484 </file>
17485 <file name="script.js">
17486 angular.module('customControl', ['ngSanitize']).
17487 directive('contenteditable', ['$sce', function($sce) {
17488 return {
17489 restrict: 'A', // only activate on element attribute
17490 require: '?ngModel', // get a hold of NgModelController
17491 link: function(scope, element, attrs, ngModel) {
17492 if(!ngModel) return; // do nothing if no ng-model
17493
17494 // Specify how UI should be updated
17495 ngModel.$render = function() {
17496 element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
17497 };
17498
17499 // Listen for change events to enable binding
17500 element.on('blur keyup change', function() {
17501 scope.$apply(read);
17502 });
17503 read(); // initialize
17504
17505 // Write data to the model
17506 function read() {
17507 var html = element.html();
17508 // When we clear the content editable the browser leaves a <br> behind
17509 // If strip-br attribute is provided then we strip this out
17510 if( attrs.stripBr && html == '<br>' ) {
17511 html = '';
17512 }
17513 ngModel.$setViewValue(html);
17514 }
17515 }
17516 };
17517 }]);
17518 </file>
17519 <file name="index.html">
17520 <form name="myForm">
17521 <div contenteditable
17522 name="myWidget" ng-model="userContent"
17523 strip-br="true"
17524 required>Change me!</div>
17525 <span ng-show="myForm.myWidget.$error.required">Required!</span>
17526 <hr>
17527 <textarea ng-model="userContent"></textarea>
17528 </form>
17529 </file>
17530 <file name="protractor.js" type="protractor">
17531 it('should data-bind and become invalid', function() {
17532 if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
17533 // SafariDriver can't handle contenteditable
17534 // and Firefox driver can't clear contenteditables very well
17535 return;
17536 }
17537 var contentEditable = element(by.css('[contenteditable]'));
17538 var content = 'Change me!';
17539
17540 expect(contentEditable.getText()).toEqual(content);
17541
17542 contentEditable.clear();
17543 contentEditable.sendKeys(protractor.Key.BACK_SPACE);
17544 expect(contentEditable.getText()).toEqual('');
17545 expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
17546 });
17547 </file>
17548 * </example>
17549 *
17550 *
17551 */
17552 var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate',
17553 function($scope, $exceptionHandler, $attr, $element, $parse, $animate) {
17554 this.$viewValue = Number.NaN;
17555 this.$modelValue = Number.NaN;
17556 this.$parsers = [];
17557 this.$formatters = [];
17558 this.$viewChangeListeners = [];
17559 this.$pristine = true;
17560 this.$dirty = false;
17561 this.$valid = true;
17562 this.$invalid = false;
17563 this.$name = $attr.name;
17564
17565 var ngModelGet = $parse($attr.ngModel),
17566 ngModelSet = ngModelGet.assign;
17567
17568 if (!ngModelSet) {
17569 throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
17570 $attr.ngModel, startingTag($element));
17571 }
17572
17573 /**
17574 * @ngdoc method
17575 * @name ngModel.NgModelController#$render
17576 *
17577 * @description
17578 * Called when the view needs to be updated. It is expected that the user of the ng-model
17579 * directive will implement this method.
17580 */
17581 this.$render = noop;
17582
17583 /**
17584 * @ngdoc method
17585 * @name ngModel.NgModelController#$isEmpty
17586 *
17587 * @description
17588 * This is called when we need to determine if the value of the input is empty.
17589 *
17590 * For instance, the required directive does this to work out if the input has data or not.
17591 * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
17592 *
17593 * You can override this for input directives whose concept of being empty is different to the
17594 * default. The `checkboxInputType` directive does this because in its case a value of `false`
17595 * implies empty.
17596 *
17597 * @param {*} value Reference to check.
17598 * @returns {boolean} True if `value` is empty.
17599 */
17600 this.$isEmpty = function(value) {
17601 return isUndefined(value) || value === '' || value === null || value !== value;
17602 };
17603
17604 var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
17605 invalidCount = 0, // used to easily determine if we are valid
17606 $error = this.$error = {}; // keep invalid keys here
17607
17608
17609 // Setup initial state of the control
17610 $element.addClass(PRISTINE_CLASS);
17611 toggleValidCss(true);
17612
17613 // convenience method for easy toggling of classes
17614 function toggleValidCss(isValid, validationErrorKey) {
17615 validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
17616 $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
17617 $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
17618 }
17619
17620 /**
17621 * @ngdoc method
17622 * @name ngModel.NgModelController#$setValidity
17623 *
17624 * @description
17625 * Change the validity state, and notifies the form when the control changes validity. (i.e. it
17626 * does not notify form if given validator is already marked as invalid).
17627 *
17628 * This method should be called by validators - i.e. the parser or formatter functions.
17629 *
17630 * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
17631 * to `$error[validationErrorKey]=!isValid` so that it is available for data-binding.
17632 * The `validationErrorKey` should be in camelCase and will get converted into dash-case
17633 * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
17634 * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
17635 * @param {boolean} isValid Whether the current state is valid (true) or invalid (false).
17636 */
17637 this.$setValidity = function(validationErrorKey, isValid) {
17638 // Purposeful use of ! here to cast isValid to boolean in case it is undefined
17639 // jshint -W018
17640 if ($error[validationErrorKey] === !isValid) return;
17641 // jshint +W018
17642
17643 if (isValid) {
17644 if ($error[validationErrorKey]) invalidCount--;
17645 if (!invalidCount) {
17646 toggleValidCss(true);
17647 this.$valid = true;
17648 this.$invalid = false;
17649 }
17650 } else {
17651 toggleValidCss(false);
17652 this.$invalid = true;
17653 this.$valid = false;
17654 invalidCount++;
17655 }
17656
17657 $error[validationErrorKey] = !isValid;
17658 toggleValidCss(isValid, validationErrorKey);
17659
17660 parentForm.$setValidity(validationErrorKey, isValid, this);
17661 };
17662
17663 /**
17664 * @ngdoc method
17665 * @name ngModel.NgModelController#$setPristine
17666 *
17667 * @description
17668 * Sets the control to its pristine state.
17669 *
17670 * This method can be called to remove the 'ng-dirty' class and set the control to its pristine
17671 * state (ng-pristine class).
17672 */
17673 this.$setPristine = function () {
17674 this.$dirty = false;
17675 this.$pristine = true;
17676 $animate.removeClass($element, DIRTY_CLASS);
17677 $animate.addClass($element, PRISTINE_CLASS);
17678 };
17679
17680 /**
17681 * @ngdoc method
17682 * @name ngModel.NgModelController#$setViewValue
17683 *
17684 * @description
17685 * Update the view value.
17686 *
17687 * This method should be called when the view value changes, typically from within a DOM event handler.
17688 * For example {@link ng.directive:input input} and
17689 * {@link ng.directive:select select} directives call it.
17690 *
17691 * It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
17692 * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
17693 * `$modelValue` and the **expression** specified in the `ng-model` attribute.
17694 *
17695 * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
17696 *
17697 * Note that calling this function does not trigger a `$digest`.
17698 *
17699 * @param {string} value Value from the view.
17700 */
17701 this.$setViewValue = function(value) {
17702 this.$viewValue = value;
17703
17704 // change to dirty
17705 if (this.$pristine) {
17706 this.$dirty = true;
17707 this.$pristine = false;
17708 $animate.removeClass($element, PRISTINE_CLASS);
17709 $animate.addClass($element, DIRTY_CLASS);
17710 parentForm.$setDirty();
17711 }
17712
17713 forEach(this.$parsers, function(fn) {
17714 value = fn(value);
17715 });
17716
17717 if (this.$modelValue !== value) {
17718 this.$modelValue = value;
17719 ngModelSet($scope, value);
17720 forEach(this.$viewChangeListeners, function(listener) {
17721 try {
17722 listener();
17723 } catch(e) {
17724 $exceptionHandler(e);
17725 }
17726 });
17727 }
17728 };
17729
17730 // model -> value
17731 var ctrl = this;
17732
17733 $scope.$watch(function ngModelWatch() {
17734 var value = ngModelGet($scope);
17735
17736 // if scope model value and ngModel value are out of sync
17737 if (ctrl.$modelValue !== value) {
17738
17739 var formatters = ctrl.$formatters,
17740 idx = formatters.length;
17741
17742 ctrl.$modelValue = value;
17743 while(idx--) {
17744 value = formatters[idx](value);
17745 }
17746
17747 if (ctrl.$viewValue !== value) {
17748 ctrl.$viewValue = value;
17749 ctrl.$render();
17750 }
17751 }
17752
17753 return value;
17754 });
17755 }];
17756
17757
17758 /**
17759 * @ngdoc directive
17760 * @name ngModel
17761 *
17762 * @element input
17763 *
17764 * @description
17765 * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
17766 * property on the scope using {@link ngModel.NgModelController NgModelController},
17767 * which is created and exposed by this directive.
17768 *
17769 * `ngModel` is responsible for:
17770 *
17771 * - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
17772 * require.
17773 * - Providing validation behavior (i.e. required, number, email, url).
17774 * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors).
17775 * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations.
17776 * - Registering the control with its parent {@link ng.directive:form form}.
17777 *
17778 * Note: `ngModel` will try to bind to the property given by evaluating the expression on the
17779 * current scope. If the property doesn't already exist on this scope, it will be created
17780 * implicitly and added to the scope.
17781 *
17782 * For best practices on using `ngModel`, see:
17783 *
17784 * - [https://github.com/angular/angular.js/wiki/Understanding-Scopes]
17785 *
17786 * For basic examples, how to use `ngModel`, see:
17787 *
17788 * - {@link ng.directive:input input}
17789 * - {@link input[text] text}
17790 * - {@link input[checkbox] checkbox}
17791 * - {@link input[radio] radio}
17792 * - {@link input[number] number}
17793 * - {@link input[email] email}
17794 * - {@link input[url] url}
17795 * - {@link ng.directive:select select}
17796 * - {@link ng.directive:textarea textarea}
17797 *
17798 * # CSS classes
17799 * The following CSS classes are added and removed on the associated input/select/textarea element
17800 * depending on the validity of the model.
17801 *
17802 * - `ng-valid` is set if the model is valid.
17803 * - `ng-invalid` is set if the model is invalid.
17804 * - `ng-pristine` is set if the model is pristine.
17805 * - `ng-dirty` is set if the model is dirty.
17806 *
17807 * Keep in mind that ngAnimate can detect each of these classes when added and removed.
17808 *
17809 * ## Animation Hooks
17810 *
17811 * Animations within models are triggered when any of the associated CSS classes are added and removed
17812 * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
17813 * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
17814 * The animations that are triggered within ngModel are similar to how they work in ngClass and
17815 * animations can be hooked into using CSS transitions, keyframes as well as JS animations.
17816 *
17817 * The following example shows a simple way to utilize CSS transitions to style an input element
17818 * that has been rendered as invalid after it has been validated:
17819 *
17820 * <pre>
17821 * //be sure to include ngAnimate as a module to hook into more
17822 * //advanced animations
17823 * .my-input {
17824 * transition:0.5s linear all;
17825 * background: white;
17826 * }
17827 * .my-input.ng-invalid {
17828 * background: red;
17829 * color:white;
17830 * }
17831 * </pre>
17832 *
17833 * @example
17834 * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample">
17835 <file name="index.html">
17836 <script>
17837 angular.module('inputExample', [])
17838 .controller('ExampleController', ['$scope', function($scope) {
17839 $scope.val = '1';
17840 }]);
17841 </script>
17842 <style>
17843 .my-input {
17844 -webkit-transition:all linear 0.5s;
17845 transition:all linear 0.5s;
17846 background: transparent;
17847 }
17848 .my-input.ng-invalid {
17849 color:white;
17850 background: red;
17851 }
17852 </style>
17853 Update input to see transitions when valid/invalid.
17854 Integer is a valid value.
17855 <form name="testForm" ng-controller="ExampleController">
17856 <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" />
17857 </form>
17858 </file>
17859 * </example>
17860 */
17861 var ngModelDirective = function() {
17862 return {
17863 require: ['ngModel', '^?form'],
17864 controller: NgModelController,
17865 link: function(scope, element, attr, ctrls) {
17866 // notify others, especially parent forms
17867
17868 var modelCtrl = ctrls[0],
17869 formCtrl = ctrls[1] || nullFormCtrl;
17870
17871 formCtrl.$addControl(modelCtrl);
17872
17873 scope.$on('$destroy', function() {
17874 formCtrl.$removeControl(modelCtrl);
17875 });
17876 }
17877 };
17878 };
17879
17880
17881 /**
17882 * @ngdoc directive
17883 * @name ngChange
17884 *
17885 * @description
17886 * Evaluate the given expression when the user changes the input.
17887 * The expression is evaluated immediately, unlike the JavaScript onchange event
17888 * which only triggers at the end of a change (usually, when the user leaves the
17889 * form element or presses the return key).
17890 * The expression is not evaluated when the value change is coming from the model.
17891 *
17892 * Note, this directive requires `ngModel` to be present.
17893 *
17894 * @element input
17895 * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
17896 * in input value.
17897 *
17898 * @example
17899 * <example name="ngChange-directive" module="changeExample">
17900 * <file name="index.html">
17901 * <script>
17902 * angular.module('changeExample', [])
17903 * .controller('ExampleController', ['$scope', function($scope) {
17904 * $scope.counter = 0;
17905 * $scope.change = function() {
17906 * $scope.counter++;
17907 * };
17908 * }]);
17909 * </script>
17910 * <div ng-controller="ExampleController">
17911 * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" />
17912 * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" />
17913 * <label for="ng-change-example2">Confirmed</label><br />
17914 * <tt>debug = {{confirmed}}</tt><br/>
17915 * <tt>counter = {{counter}}</tt><br/>
17916 * </div>
17917 * </file>
17918 * <file name="protractor.js" type="protractor">
17919 * var counter = element(by.binding('counter'));
17920 * var debug = element(by.binding('confirmed'));
17921 *
17922 * it('should evaluate the expression if changing from view', function() {
17923 * expect(counter.getText()).toContain('0');
17924 *
17925 * element(by.id('ng-change-example1')).click();
17926 *
17927 * expect(counter.getText()).toContain('1');
17928 * expect(debug.getText()).toContain('true');
17929 * });
17930 *
17931 * it('should not evaluate the expression if changing from model', function() {
17932 * element(by.id('ng-change-example2')).click();
17933
17934 * expect(counter.getText()).toContain('0');
17935 * expect(debug.getText()).toContain('true');
17936 * });
17937 * </file>
17938 * </example>
17939 */
17940 var ngChangeDirective = valueFn({
17941 require: 'ngModel',
17942 link: function(scope, element, attr, ctrl) {
17943 ctrl.$viewChangeListeners.push(function() {
17944 scope.$eval(attr.ngChange);
17945 });
17946 }
17947 });
17948
17949
17950 var requiredDirective = function() {
17951 return {
17952 require: '?ngModel',
17953 link: function(scope, elm, attr, ctrl) {
17954 if (!ctrl) return;
17955 attr.required = true; // force truthy in case we are on non input element
17956
17957 var validator = function(value) {
17958 if (attr.required && ctrl.$isEmpty(value)) {
17959 ctrl.$setValidity('required', false);
17960 return;
17961 } else {
17962 ctrl.$setValidity('required', true);
17963 return value;
17964 }
17965 };
17966
17967 ctrl.$formatters.push(validator);
17968 ctrl.$parsers.unshift(validator);
17969
17970 attr.$observe('required', function() {
17971 validator(ctrl.$viewValue);
17972 });
17973 }
17974 };
17975 };
17976
17977
17978 /**
17979 * @ngdoc directive
17980 * @name ngList
17981 *
17982 * @description
17983 * Text input that converts between a delimited string and an array of strings. The delimiter
17984 * can be a fixed string (by default a comma) or a regular expression.
17985 *
17986 * @element input
17987 * @param {string=} ngList optional delimiter that should be used to split the value. If
17988 * specified in form `/something/` then the value will be converted into a regular expression.
17989 *
17990 * @example
17991 <example name="ngList-directive" module="listExample">
17992 <file name="index.html">
17993 <script>
17994 angular.module('listExample', [])
17995 .controller('ExampleController', ['$scope', function($scope) {
17996 $scope.names = ['igor', 'misko', 'vojta'];
17997 }]);
17998 </script>
17999 <form name="myForm" ng-controller="ExampleController">
18000 List: <input name="namesInput" ng-model="names" ng-list required>
18001 <span class="error" ng-show="myForm.namesInput.$error.required">
18002 Required!</span>
18003 <br>
18004 <tt>names = {{names}}</tt><br/>
18005 <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
18006 <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
18007 <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
18008 <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
18009 </form>
18010 </file>
18011 <file name="protractor.js" type="protractor">
18012 var listInput = element(by.model('names'));
18013 var names = element(by.binding('{{names}}'));
18014 var valid = element(by.binding('myForm.namesInput.$valid'));
18015 var error = element(by.css('span.error'));
18016
18017 it('should initialize to model', function() {
18018 expect(names.getText()).toContain('["igor","misko","vojta"]');
18019 expect(valid.getText()).toContain('true');
18020 expect(error.getCssValue('display')).toBe('none');
18021 });
18022
18023 it('should be invalid if empty', function() {
18024 listInput.clear();
18025 listInput.sendKeys('');
18026
18027 expect(names.getText()).toContain('');
18028 expect(valid.getText()).toContain('false');
18029 expect(error.getCssValue('display')).not.toBe('none'); });
18030 </file>
18031 </example>
18032 */
18033 var ngListDirective = function() {
18034 return {
18035 require: 'ngModel',
18036 link: function(scope, element, attr, ctrl) {
18037 var match = /\/(.*)\//.exec(attr.ngList),
18038 separator = match && new RegExp(match[1]) || attr.ngList || ',';
18039
18040 var parse = function(viewValue) {
18041 // If the viewValue is invalid (say required but empty) it will be `undefined`
18042 if (isUndefined(viewValue)) return;
18043
18044 var list = [];
18045
18046 if (viewValue) {
18047 forEach(viewValue.split(separator), function(value) {
18048 if (value) list.push(trim(value));
18049 });
18050 }
18051
18052 return list;
18053 };
18054
18055 ctrl.$parsers.push(parse);
18056 ctrl.$formatters.push(function(value) {
18057 if (isArray(value)) {
18058 return value.join(', ');
18059 }
18060
18061 return undefined;
18062 });
18063
18064 // Override the standard $isEmpty because an empty array means the input is empty.
18065 ctrl.$isEmpty = function(value) {
18066 return !value || !value.length;
18067 };
18068 }
18069 };
18070 };
1807122011
1807222012
1807322013 var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
1807622016 * @name ngValue
1807722017 *
1807822018 * @description
18079 * Binds the given expression to the value of `input[select]` or `input[radio]`, so
18080 * that when the element is selected, the `ngModel` of that element is set to the
18081 * bound value.
18082 *
18083 * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as
18084 * shown below.
22019 * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`},
22020 * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to
22021 * the bound value.
22022 *
22023 * `ngValue` is useful when dynamically generating lists of radio buttons using
22024 * {@link ngRepeat `ngRepeat`}, as shown below.
22025 *
22026 * Likewise, `ngValue` can be used to generate `<option>` elements for
22027 * the {@link select `select`} element. In that case however, only strings are supported
22028 * for the `value `attribute, so the resulting `ngModel` will always be a string.
22029 * Support for `select` models with non-string values is available via `ngOptions`.
1808522030 *
1808622031 * @element input
1808722032 * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
1812522070 */
1812622071 var ngValueDirective = function() {
1812722072 return {
22073 restrict: 'A',
1812822074 priority: 100,
1812922075 compile: function(tpl, tplAttr) {
1813022076 if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
1817722123 }]);
1817822124 </script>
1817922125 <div ng-controller="ExampleController">
18180 Enter name: <input type="text" ng-model="name"><br>
22126 <label>Enter name: <input type="text" ng-model="name"></label><br>
1818122127 Hello <span ng-bind="name"></span>!
1818222128 </div>
1818322129 </file>
1819322139 </file>
1819422140 </example>
1819522141 */
18196 var ngBindDirective = ngDirective({
18197 compile: function(templateElement) {
18198 templateElement.addClass('ng-binding');
18199 return function (scope, element, attr) {
18200 element.data('$binding', attr.ngBind);
18201 scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
18202 // We are purposefully using == here rather than === because we want to
18203 // catch when value is "null or undefined"
18204 // jshint -W041
18205 element.text(value == undefined ? '' : value);
18206 });
18207 };
18208 }
18209 });
22142 var ngBindDirective = ['$compile', function($compile) {
22143 return {
22144 restrict: 'AC',
22145 compile: function ngBindCompile(templateElement) {
22146 $compile.$$addBindingClass(templateElement);
22147 return function ngBindLink(scope, element, attr) {
22148 $compile.$$addBindingInfo(element, attr.ngBind);
22149 element = element[0];
22150 scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
22151 element.textContent = value === undefined ? '' : value;
22152 });
22153 };
22154 }
22155 };
22156 }];
1821022157
1821122158
1821222159 /**
1823122178 <file name="index.html">
1823222179 <script>
1823322180 angular.module('bindExample', [])
18234 .controller('ExampleController', ['$scope', function ($scope) {
22181 .controller('ExampleController', ['$scope', function($scope) {
1823522182 $scope.salutation = 'Hello';
1823622183 $scope.name = 'World';
1823722184 }]);
1823822185 </script>
1823922186 <div ng-controller="ExampleController">
18240 Salutation: <input type="text" ng-model="salutation"><br>
18241 Name: <input type="text" ng-model="name"><br>
22187 <label>Salutation: <input type="text" ng-model="salutation"></label><br>
22188 <label>Name: <input type="text" ng-model="name"></label><br>
1824222189 <pre ng-bind-template="{{salutation}} {{name}}!"></pre>
1824322190 </div>
1824422191 </file>
1826022207 </file>
1826122208 </example>
1826222209 */
18263 var ngBindTemplateDirective = ['$interpolate', function($interpolate) {
18264 return function(scope, element, attr) {
18265 // TODO: move this to scenario runner
18266 var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate));
18267 element.addClass('ng-binding').data('$binding', interpolateFn);
18268 attr.$observe('ngBindTemplate', function(value) {
18269 element.text(value);
18270 });
22210 var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) {
22211 return {
22212 compile: function ngBindTemplateCompile(templateElement) {
22213 $compile.$$addBindingClass(templateElement);
22214 return function ngBindTemplateLink(scope, element, attr) {
22215 var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate));
22216 $compile.$$addBindingInfo(element, interpolateFn.expressions);
22217 element = element[0];
22218 attr.$observe('ngBindTemplate', function(value) {
22219 element.textContent = value === undefined ? '' : value;
22220 });
22221 };
22222 }
1827122223 };
1827222224 }];
1827322225
1827722229 * @name ngBindHtml
1827822230 *
1827922231 * @description
18280 * Creates a binding that will innerHTML the result of evaluating the `expression` into the current
18281 * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link
18282 * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize`
18283 * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in
18284 * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to
22232 * Evaluates the expression and inserts the resulting HTML into the element in a secure way. By default,
22233 * the resulting HTML content will be sanitized using the {@link ngSanitize.$sanitize $sanitize} service.
22234 * To utilize this functionality, ensure that `$sanitize` is available, for example, by including {@link
22235 * ngSanitize} in your module's dependencies (not in core Angular). In order to use {@link ngSanitize}
22236 * in your module's dependencies, you need to include "angular-sanitize.js" in your application.
22237 *
22238 * You may also bypass sanitization for values you know are safe. To do so, bind to
1828522239 * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example
18286 * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}.
22240 * under {@link ng.$sce#show-me-an-example-using-sce- Strict Contextual Escaping (SCE)}.
1828722241 *
1828822242 * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you
1828922243 * will have an exception (instead of an exploit.)
1831722271 </file>
1831822272 </example>
1831922273 */
18320 var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) {
22274 var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) {
1832122275 return {
18322 compile: function (tElement) {
18323 tElement.addClass('ng-binding');
18324
18325 return function (scope, element, attr) {
18326 element.data('$binding', attr.ngBindHtml);
18327
18328 var parsed = $parse(attr.ngBindHtml);
18329
18330 function getStringValue() {
18331 return (parsed(scope) || '').toString();
18332 }
18333
18334 scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
18335 element.html($sce.getTrustedHtml(parsed(scope)) || '');
22276 restrict: 'A',
22277 compile: function ngBindHtmlCompile(tElement, tAttrs) {
22278 var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
22279 var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
22280 return (value || '').toString();
22281 });
22282 $compile.$$addBindingClass(tElement);
22283
22284 return function ngBindHtmlLink(scope, element, attr) {
22285 $compile.$$addBindingInfo(element, attr.ngBindHtml);
22286
22287 scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
22288 // we re-evaluate the expr because we want a TrustedValueHolderType
22289 // for $sce, not a string
22290 element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
1833622291 });
1833722292 };
1833822293 }
1833922294 };
1834022295 }];
22296
22297 /**
22298 * @ngdoc directive
22299 * @name ngChange
22300 *
22301 * @description
22302 * Evaluate the given expression when the user changes the input.
22303 * The expression is evaluated immediately, unlike the JavaScript onchange event
22304 * which only triggers at the end of a change (usually, when the user leaves the
22305 * form element or presses the return key).
22306 *
22307 * The `ngChange` expression is only evaluated when a change in the input value causes
22308 * a new value to be committed to the model.
22309 *
22310 * It will not be evaluated:
22311 * * if the value returned from the `$parsers` transformation pipeline has not changed
22312 * * if the input has continued to be invalid since the model will stay `null`
22313 * * if the model is changed programmatically and not by a change to the input value
22314 *
22315 *
22316 * Note, this directive requires `ngModel` to be present.
22317 *
22318 * @element input
22319 * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
22320 * in input value.
22321 *
22322 * @example
22323 * <example name="ngChange-directive" module="changeExample">
22324 * <file name="index.html">
22325 * <script>
22326 * angular.module('changeExample', [])
22327 * .controller('ExampleController', ['$scope', function($scope) {
22328 * $scope.counter = 0;
22329 * $scope.change = function() {
22330 * $scope.counter++;
22331 * };
22332 * }]);
22333 * </script>
22334 * <div ng-controller="ExampleController">
22335 * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" />
22336 * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" />
22337 * <label for="ng-change-example2">Confirmed</label><br />
22338 * <tt>debug = {{confirmed}}</tt><br/>
22339 * <tt>counter = {{counter}}</tt><br/>
22340 * </div>
22341 * </file>
22342 * <file name="protractor.js" type="protractor">
22343 * var counter = element(by.binding('counter'));
22344 * var debug = element(by.binding('confirmed'));
22345 *
22346 * it('should evaluate the expression if changing from view', function() {
22347 * expect(counter.getText()).toContain('0');
22348 *
22349 * element(by.id('ng-change-example1')).click();
22350 *
22351 * expect(counter.getText()).toContain('1');
22352 * expect(debug.getText()).toContain('true');
22353 * });
22354 *
22355 * it('should not evaluate the expression if changing from model', function() {
22356 * element(by.id('ng-change-example2')).click();
22357
22358 * expect(counter.getText()).toContain('0');
22359 * expect(debug.getText()).toContain('true');
22360 * });
22361 * </file>
22362 * </example>
22363 */
22364 var ngChangeDirective = valueFn({
22365 restrict: 'A',
22366 require: 'ngModel',
22367 link: function(scope, element, attr, ctrl) {
22368 ctrl.$viewChangeListeners.push(function() {
22369 scope.$eval(attr.ngChange);
22370 });
22371 }
22372 });
1834122373
1834222374 function classDirective(name, selector) {
1834322375 name = 'ngClass' + name;
1837722409 attr.$removeClass(newClasses);
1837822410 }
1837922411
18380 function digestClassCounts (classes, count) {
18381 var classCounts = element.data('$classCounts') || {};
22412 function digestClassCounts(classes, count) {
22413 // Use createMap() to prevent class assumptions involving property
22414 // names in Object.prototype
22415 var classCounts = element.data('$classCounts') || createMap();
1838222416 var classesToUpdate = [];
18383 forEach(classes, function (className) {
22417 forEach(classes, function(className) {
1838422418 if (count > 0 || classCounts[className]) {
1838522419 classCounts[className] = (classCounts[className] || 0) + count;
1838622420 if (classCounts[className] === +(count > 0)) {
1839222426 return classesToUpdate.join(' ');
1839322427 }
1839422428
18395 function updateClasses (oldClasses, newClasses) {
22429 function updateClasses(oldClasses, newClasses) {
1839622430 var toAdd = arrayDifference(newClasses, oldClasses);
1839722431 var toRemove = arrayDifference(oldClasses, newClasses);
22432 toAdd = digestClassCounts(toAdd, 1);
1839822433 toRemove = digestClassCounts(toRemove, -1);
18399 toAdd = digestClassCounts(toAdd, 1);
18400
18401 if (toAdd.length === 0) {
22434 if (toAdd && toAdd.length) {
22435 $animate.addClass(element, toAdd);
22436 }
22437 if (toRemove && toRemove.length) {
1840222438 $animate.removeClass(element, toRemove);
18403 } else if (toRemove.length === 0) {
18404 $animate.addClass(element, toAdd);
18405 } else {
18406 $animate.setClass(element, toAdd, toRemove);
1840722439 }
1840822440 }
1840922441
1842622458 var values = [];
1842722459
1842822460 outer:
18429 for(var i = 0; i < tokens1.length; i++) {
22461 for (var i = 0; i < tokens1.length; i++) {
1843022462 var token = tokens1[i];
18431 for(var j = 0; j < tokens2.length; j++) {
18432 if(token == tokens2[j]) continue outer;
22463 for (var j = 0; j < tokens2.length; j++) {
22464 if (token == tokens2[j]) continue outer;
1843322465 }
1843422466 values.push(token);
1843522467 }
1843622468 return values;
1843722469 }
1843822470
18439 function arrayClasses (classVal) {
22471 function arrayClasses(classVal) {
22472 var classes = [];
1844022473 if (isArray(classVal)) {
18441 return classVal;
22474 forEach(classVal, function(v) {
22475 classes = classes.concat(arrayClasses(v));
22476 });
22477 return classes;
1844222478 } else if (isString(classVal)) {
1844322479 return classVal.split(' ');
1844422480 } else if (isObject(classVal)) {
18445 var classes = [], i = 0;
1844622481 forEach(classVal, function(v, k) {
1844722482 if (v) {
1844822483 classes = classes.concat(k.split(' '));
1847022505 * 1. If the expression evaluates to a string, the string should be one or more space-delimited class
1847122506 * names.
1847222507 *
18473 * 2. If the expression evaluates to an array, each element of the array should be a string that is
18474 * one or more space-delimited class names.
18475 *
18476 * 3. If the expression evaluates to an object, then for each key-value pair of the
22508 * 2. If the expression evaluates to an object, then for each key-value pair of the
1847722509 * object with a truthy value the corresponding key is used as a class name.
1847822510 *
22511 * 3. If the expression evaluates to an array, each element of the array should either be a string as in
22512 * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array
22513 * to give you more control over what CSS classes appear. See the code below for an example of this.
22514 *
22515 *
1847922516 * The directive won't add duplicate classes if a particular class was already set.
1848022517 *
18481 * When the expression changes, the previously added classes are removed and only then the
18482 * new classes are added.
22518 * When the expression changes, the previously added classes are removed and only then are the
22519 * new classes added.
1848322520 *
1848422521 * @animations
18485 * add - happens just before the class is applied to the element
18486 * remove - happens just before the class is removed from the element
22522 * **add** - happens just before the class is applied to the elements
22523 *
22524 * **remove** - happens just before the class is removed from the element
1848722525 *
1848822526 * @element ANY
1848922527 * @param {expression} ngClass {@link guide/expression Expression} to eval. The result
1849522533 * @example Example that demonstrates basic bindings via ngClass directive.
1849622534 <example>
1849722535 <file name="index.html">
18498 <p ng-class="{strike: deleted, bold: important, red: error}">Map Syntax Example</p>
18499 <input type="checkbox" ng-model="deleted"> deleted (apply "strike" class)<br>
18500 <input type="checkbox" ng-model="important"> important (apply "bold" class)<br>
18501 <input type="checkbox" ng-model="error"> error (apply "red" class)
22536 <p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p>
22537 <label>
22538 <input type="checkbox" ng-model="deleted">
22539 deleted (apply "strike" class)
22540 </label><br>
22541 <label>
22542 <input type="checkbox" ng-model="important">
22543 important (apply "bold" class)
22544 </label><br>
22545 <label>
22546 <input type="checkbox" ng-model="error">
22547 error (apply "has-error" class)
22548 </label>
1850222549 <hr>
1850322550 <p ng-class="style">Using String Syntax</p>
18504 <input type="text" ng-model="style" placeholder="Type: bold strike red">
22551 <input type="text" ng-model="style"
22552 placeholder="Type: bold strike red" aria-label="Type: bold strike red">
1850522553 <hr>
1850622554 <p ng-class="[style1, style2, style3]">Using Array Syntax</p>
18507 <input ng-model="style1" placeholder="Type: bold, strike or red"><br>
18508 <input ng-model="style2" placeholder="Type: bold, strike or red"><br>
18509 <input ng-model="style3" placeholder="Type: bold, strike or red"><br>
22555 <input ng-model="style1"
22556 placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red"><br>
22557 <input ng-model="style2"
22558 placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 2"><br>
22559 <input ng-model="style3"
22560 placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 3"><br>
22561 <hr>
22562 <p ng-class="[style4, {orange: warning}]">Using Array and Map Syntax</p>
22563 <input ng-model="style4" placeholder="Type: bold, strike" aria-label="Type: bold, strike"><br>
22564 <label><input type="checkbox" ng-model="warning"> warning (apply "orange" class)</label>
1851022565 </file>
1851122566 <file name="style.css">
1851222567 .strike {
18513 text-decoration: line-through;
22568 text-decoration: line-through;
1851422569 }
1851522570 .bold {
1851622571 font-weight: bold;
1851822573 .red {
1851922574 color: red;
1852022575 }
22576 .has-error {
22577 color: red;
22578 background-color: yellow;
22579 }
22580 .orange {
22581 color: orange;
22582 }
1852122583 </file>
1852222584 <file name="protractor.js" type="protractor">
1852322585 var ps = element.all(by.css('p'));
1852522587 it('should let you toggle the class', function() {
1852622588
1852722589 expect(ps.first().getAttribute('class')).not.toMatch(/bold/);
18528 expect(ps.first().getAttribute('class')).not.toMatch(/red/);
22590 expect(ps.first().getAttribute('class')).not.toMatch(/has-error/);
1852922591
1853022592 element(by.model('important')).click();
1853122593 expect(ps.first().getAttribute('class')).toMatch(/bold/);
1853222594
1853322595 element(by.model('error')).click();
18534 expect(ps.first().getAttribute('class')).toMatch(/red/);
22596 expect(ps.first().getAttribute('class')).toMatch(/has-error/);
1853522597 });
1853622598
1853722599 it('should let you toggle string example', function() {
1854222604 });
1854322605
1854422606 it('array example should have 3 classes', function() {
18545 expect(ps.last().getAttribute('class')).toBe('');
22607 expect(ps.get(2).getAttribute('class')).toBe('');
1854622608 element(by.model('style1')).sendKeys('bold');
1854722609 element(by.model('style2')).sendKeys('strike');
1854822610 element(by.model('style3')).sendKeys('red');
18549 expect(ps.last().getAttribute('class')).toBe('bold strike red');
22611 expect(ps.get(2).getAttribute('class')).toBe('bold strike red');
22612 });
22613
22614 it('array with map example should have 2 classes', function() {
22615 expect(ps.last().getAttribute('class')).toBe('');
22616 element(by.model('style4')).sendKeys('bold');
22617 element(by.model('warning')).click();
22618 expect(ps.last().getAttribute('class')).toBe('bold orange');
1855022619 });
1855122620 </file>
1855222621 </example>
1859622665 The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
1859722666 Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder
1859822667 any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure
18599 to view the step by step details of {@link ngAnimate.$animate#addclass $animate.addClass} and
18600 {@link ngAnimate.$animate#removeclass $animate.removeClass}.
22668 to view the step by step details of {@link $animate#addClass $animate.addClass} and
22669 {@link $animate#removeClass $animate.removeClass}.
1860122670 */
1860222671 var ngClassDirective = classDirective('', true);
1860322672
1873022799 * document; alternatively, the css rule above must be included in the external stylesheet of the
1873122800 * application.
1873222801 *
18733 * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they
18734 * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css
18735 * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below.
18736 *
1873722802 * @element ANY
1873822803 *
1873922804 * @example
1874022805 <example>
1874122806 <file name="index.html">
1874222807 <div id="template1" ng-cloak>{{ 'hello' }}</div>
18743 <div id="template2" ng-cloak class="ng-cloak">{{ 'hello IE7' }}</div>
22808 <div id="template2" class="ng-cloak">{{ 'world' }}</div>
1874422809 </file>
1874522810 <file name="protractor.js" type="protractor">
1874622811 it('should remove the template directive and css class', function() {
1878322848 *
1878422849 * @element ANY
1878522850 * @scope
18786 * @param {expression} ngController Name of a globally accessible constructor function or an
18787 * {@link guide/expression expression} that on the current scope evaluates to a
18788 * constructor function. The controller instance can be published into a scope property
18789 * by specifying `as propertyName`.
22851 * @priority 500
22852 * @param {expression} ngController Name of a constructor function registered with the current
22853 * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression}
22854 * that on the current scope evaluates to a constructor function.
22855 *
22856 * The controller instance can be published into a scope property by specifying
22857 * `ng-controller="as propertyName"`.
22858 *
22859 * If the current `$controllerProvider` is configured to use globals (via
22860 * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may
22861 * also be the name of a globally accessible constructor function (not recommended).
1879022862 *
1879122863 * @example
1879222864 * Here is a simple form for editing user contact information. Adding, removing, clearing, and
1881722889 * <example name="ngControllerAs" module="controllerAsExample">
1881822890 * <file name="index.html">
1881922891 * <div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings">
18820 * Name: <input type="text" ng-model="settings.name"/>
18821 * [ <a href="" ng-click="settings.greet()">greet</a> ]<br/>
22892 * <label>Name: <input type="text" ng-model="settings.name"/></label>
22893 * <button ng-click="settings.greet()">greet</button><br/>
1882222894 * Contact:
1882322895 * <ul>
1882422896 * <li ng-repeat="contact in settings.contacts">
18825 * <select ng-model="contact.type">
22897 * <select ng-model="contact.type" aria-label="Contact method" id="select_{{$index}}">
1882622898 * <option>phone</option>
1882722899 * <option>email</option>
1882822900 * </select>
18829 * <input type="text" ng-model="contact.value"/>
18830 * [ <a href="" ng-click="settings.clearContact(contact)">clear</a>
18831 * | <a href="" ng-click="settings.removeContact(contact)">X</a> ]
22901 * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
22902 * <button ng-click="settings.clearContact(contact)">clear</button>
22903 * <button ng-click="settings.removeContact(contact)" aria-label="Remove">X</button>
1883222904 * </li>
18833 * <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li>
22905 * <li><button ng-click="settings.addContact()">add</button></li>
1883422906 * </ul>
1883522907 * </div>
1883622908 * </file>
1888022952 * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
1888122953 * .toBe('[email protected]');
1888222954 *
18883 * firstRepeat.element(by.linkText('clear')).click();
22955 * firstRepeat.element(by.buttonText('clear')).click();
1888422956 *
1888522957 * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
1888622958 * .toBe('');
1888722959 *
18888 * container.element(by.linkText('add')).click();
22960 * container.element(by.buttonText('add')).click();
1888922961 *
1889022962 * expect(container.element(by.repeater('contact in settings.contacts').row(2))
1889122963 * .element(by.model('contact.value'))
1890022972 * <example name="ngController" module="controllerExample">
1890122973 * <file name="index.html">
1890222974 * <div id="ctrl-exmpl" ng-controller="SettingsController2">
18903 * Name: <input type="text" ng-model="name"/>
18904 * [ <a href="" ng-click="greet()">greet</a> ]<br/>
22975 * <label>Name: <input type="text" ng-model="name"/></label>
22976 * <button ng-click="greet()">greet</button><br/>
1890522977 * Contact:
1890622978 * <ul>
1890722979 * <li ng-repeat="contact in contacts">
18908 * <select ng-model="contact.type">
22980 * <select ng-model="contact.type" id="select_{{$index}}">
1890922981 * <option>phone</option>
1891022982 * <option>email</option>
1891122983 * </select>
18912 * <input type="text" ng-model="contact.value"/>
18913 * [ <a href="" ng-click="clearContact(contact)">clear</a>
18914 * | <a href="" ng-click="removeContact(contact)">X</a> ]
22984 * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
22985 * <button ng-click="clearContact(contact)">clear</button>
22986 * <button ng-click="removeContact(contact)">X</button>
1891522987 * </li>
18916 * <li>[ <a href="" ng-click="addContact()">add</a> ]</li>
22988 * <li>[ <button ng-click="addContact()">add</button> ]</li>
1891722989 * </ul>
1891822990 * </div>
1891922991 * </file>
1896323035 * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
1896423036 * .toBe('[email protected]');
1896523037 *
18966 * firstRepeat.element(by.linkText('clear')).click();
23038 * firstRepeat.element(by.buttonText('clear')).click();
1896723039 *
1896823040 * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
1896923041 * .toBe('');
1897023042 *
18971 * container.element(by.linkText('add')).click();
23043 * container.element(by.buttonText('add')).click();
1897223044 *
1897323045 * expect(container.element(by.repeater('contact in contacts').row(2))
1897423046 * .element(by.model('contact.value'))
1898123053 */
1898223054 var ngControllerDirective = [function() {
1898323055 return {
23056 restrict: 'A',
1898423057 scope: true,
1898523058 controller: '@',
1898623059 priority: 500
1899523068 * @description
1899623069 * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support.
1899723070 *
18998 * This is necessary when developing things like Google Chrome Extensions.
23071 * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
1899923072 *
1900023073 * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things).
1900123074 * For Angular to be CSP compatible there are only two things that we need to do differently:
1903623109 ...
1903723110 </html>
1903823111 ```
19039 */
23112 * @example
23113 // Note: the suffix `.csp` in the example name triggers
23114 // csp mode in our http server!
23115 <example name="example.csp" module="cspExample" ng-csp="true">
23116 <file name="index.html">
23117 <div ng-controller="MainController as ctrl">
23118 <div>
23119 <button ng-click="ctrl.inc()" id="inc">Increment</button>
23120 <span id="counter">
23121 {{ctrl.counter}}
23122 </span>
23123 </div>
23124
23125 <div>
23126 <button ng-click="ctrl.evil()" id="evil">Evil</button>
23127 <span id="evilError">
23128 {{ctrl.evilError}}
23129 </span>
23130 </div>
23131 </div>
23132 </file>
23133 <file name="script.js">
23134 angular.module('cspExample', [])
23135 .controller('MainController', function() {
23136 this.counter = 0;
23137 this.inc = function() {
23138 this.counter++;
23139 };
23140 this.evil = function() {
23141 // jshint evil:true
23142 try {
23143 eval('1+2');
23144 } catch (e) {
23145 this.evilError = e.message;
23146 }
23147 };
23148 });
23149 </file>
23150 <file name="protractor.js" type="protractor">
23151 var util, webdriver;
23152
23153 var incBtn = element(by.id('inc'));
23154 var counter = element(by.id('counter'));
23155 var evilBtn = element(by.id('evil'));
23156 var evilError = element(by.id('evilError'));
23157
23158 function getAndClearSevereErrors() {
23159 return browser.manage().logs().get('browser').then(function(browserLog) {
23160 return browserLog.filter(function(logEntry) {
23161 return logEntry.level.value > webdriver.logging.Level.WARNING.value;
23162 });
23163 });
23164 }
23165
23166 function clearErrors() {
23167 getAndClearSevereErrors();
23168 }
23169
23170 function expectNoErrors() {
23171 getAndClearSevereErrors().then(function(filteredLog) {
23172 expect(filteredLog.length).toEqual(0);
23173 if (filteredLog.length) {
23174 console.log('browser console errors: ' + util.inspect(filteredLog));
23175 }
23176 });
23177 }
23178
23179 function expectError(regex) {
23180 getAndClearSevereErrors().then(function(filteredLog) {
23181 var found = false;
23182 filteredLog.forEach(function(log) {
23183 if (log.message.match(regex)) {
23184 found = true;
23185 }
23186 });
23187 if (!found) {
23188 throw new Error('expected an error that matches ' + regex);
23189 }
23190 });
23191 }
23192
23193 beforeEach(function() {
23194 util = require('util');
23195 webdriver = require('protractor/node_modules/selenium-webdriver');
23196 });
23197
23198 // For now, we only test on Chrome,
23199 // as Safari does not load the page with Protractor's injected scripts,
23200 // and Firefox webdriver always disables content security policy (#6358)
23201 if (browser.params.browser !== 'chrome') {
23202 return;
23203 }
23204
23205 it('should not report errors when the page is loaded', function() {
23206 // clear errors so we are not dependent on previous tests
23207 clearErrors();
23208 // Need to reload the page as the page is already loaded when
23209 // we come here
23210 browser.driver.getCurrentUrl().then(function(url) {
23211 browser.get(url);
23212 });
23213 expectNoErrors();
23214 });
23215
23216 it('should evaluate expressions', function() {
23217 expect(counter.getText()).toEqual('0');
23218 incBtn.click();
23219 expect(counter.getText()).toEqual('1');
23220 expectNoErrors();
23221 });
23222
23223 it('should throw and report an error when using "eval"', function() {
23224 evilBtn.click();
23225 expect(evilError.getText()).toMatch(/Content Security Policy/);
23226 expectError(/Content Security Policy/);
23227 });
23228 </file>
23229 </example>
23230 */
1904023231
1904123232 // ngCsp is not implemented as a proper directive any more, because we need it be processed while we
1904223233 // bootstrap the system (before $parse is instantiated), for this reason we just have
1906123252 <button ng-click="count = count + 1" ng-init="count=0">
1906223253 Increment
1906323254 </button>
19064 count: {{count}}
23255 <span>
23256 count: {{count}}
23257 </span>
1906523258 </file>
1906623259 <file name="protractor.js" type="protractor">
1906723260 it('should check ng-click', function() {
1907323266 </example>
1907423267 */
1907523268 /*
19076 * A directive that allows creation of custom onclick handlers that are defined as angular
19077 * expressions and are compiled and executed within the current scope.
19078 *
19079 * Events that are handled via these handler are always configured not to propagate further.
23269 * A collection of directives that allows creation of custom event handlers that are defined as
23270 * angular expressions and are compiled and executed within the current scope.
1908023271 */
1908123272 var ngEventDirectives = {};
23273
23274 // For events that might fire synchronously during DOM manipulation
23275 // we need to execute their event handlers asynchronously using $evalAsync,
23276 // so that they are not executed in an inconsistent state.
23277 var forceAsyncEvents = {
23278 'blur': true,
23279 'focus': true
23280 };
1908223281 forEach(
1908323282 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
19084 function(name) {
19085 var directiveName = directiveNormalize('ng-' + name);
19086 ngEventDirectives[directiveName] = ['$parse', function($parse) {
23283 function(eventName) {
23284 var directiveName = directiveNormalize('ng-' + eventName);
23285 ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
1908723286 return {
23287 restrict: 'A',
1908823288 compile: function($element, attr) {
19089 var fn = $parse(attr[directiveName]);
23289 // We expose the powerful $event object on the scope that provides access to the Window,
23290 // etc. that isn't protected by the fast paths in $parse. We explicitly request better
23291 // checks at the cost of speed since event handler expressions are not executed as
23292 // frequently as regular change detection.
23293 var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
1909023294 return function ngEventHandler(scope, element) {
19091 element.on(lowercase(name), function(event) {
19092 scope.$apply(function() {
23295 element.on(eventName, function(event) {
23296 var callback = function() {
1909323297 fn(scope, {$event:event});
19094 });
23298 };
23299 if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
23300 scope.$evalAsync(callback);
23301 } else {
23302 scope.$apply(callback);
23303 }
1909523304 });
1909623305 };
1909723306 }
1940823617 * @description
1940923618 * Specify custom behavior on focus event.
1941023619 *
23620 * Note: As the `focus` event is executed synchronously when calling `input.focus()`
23621 * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
23622 * during an `$apply` to ensure a consistent state.
23623 *
1941123624 * @element window, input, select, textarea, a
1941223625 * @priority 0
1941323626 * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
1942323636 *
1942423637 * @description
1942523638 * Specify custom behavior on blur event.
23639 *
23640 * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when
23641 * an element has lost focus.
23642 *
23643 * Note: As the `blur` event is executed synchronously also during DOM manipulations
23644 * (e.g. removing a focussed input),
23645 * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
23646 * during an `$apply` to ensure a consistent state.
1942623647 *
1942723648 * @element window, input, select, textarea, a
1942823649 * @priority 0
1950023721 * @ngdoc directive
1950123722 * @name ngIf
1950223723 * @restrict A
23724 * @multiElement
1950323725 *
1950423726 * @description
1950523727 * The `ngIf` directive removes or recreates a portion of the DOM tree based on an
1951523737 * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope
1951623738 * is created when the element is restored. The scope created within `ngIf` inherits from
1951723739 * its parent scope using
19518 * [prototypal inheritance](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance).
23740 * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance).
1951923741 * An important implication of this is if `ngModel` is used within `ngIf` to bind to
1952023742 * a javascript primitive defined in the parent scope. In this case any modifications made to the
1952123743 * variable within the child scope will override (hide) the value in the parent scope.
1952923751 * and `leave` effects.
1953023752 *
1953123753 * @animations
19532 * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container
19533 * leave - happens just before the ngIf contents are removed from the DOM
23754 * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container
23755 * leave - happens just before the `ngIf` contents are removed from the DOM
1953423756 *
1953523757 * @element ANY
1953623758 * @scope
1954223764 * @example
1954323765 <example module="ngAnimate" deps="angular-animate.js" animations="true">
1954423766 <file name="index.html">
19545 Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/>
23767 <label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/>
1954623768 Show when checked:
1954723769 <span ng-if="checked" class="animate-if">
19548 I'm removed when the checkbox is unchecked.
23770 This is removed when the checkbox is unchecked.
1954923771 </span>
1955023772 </file>
1955123773 <file name="animations.css">
1957423796 */
1957523797 var ngIfDirective = ['$animate', function($animate) {
1957623798 return {
23799 multiElement: true,
1957723800 transclude: 'element',
1957823801 priority: 600,
1957923802 terminal: true,
1958023803 restrict: 'A',
1958123804 $$tlb: true,
19582 link: function ($scope, $element, $attr, ctrl, $transclude) {
23805 link: function($scope, $element, $attr, ctrl, $transclude) {
1958323806 var block, childScope, previousElements;
1958423807 $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
1958523808
19586 if (toBoolean(value)) {
23809 if (value) {
1958723810 if (!childScope) {
19588 childScope = $scope.$new();
19589 $transclude(childScope, function (clone) {
23811 $transclude(function(clone, newScope) {
23812 childScope = newScope;
1959023813 clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
1959123814 // Note: We only need the first/last node of the cloned nodes.
1959223815 // However, we need to keep the reference to the jqlite wrapper as it might be changed later
1959823821 });
1959923822 }
1960023823 } else {
19601 if(previousElements) {
23824 if (previousElements) {
1960223825 previousElements.remove();
1960323826 previousElements = null;
1960423827 }
19605 if(childScope) {
23828 if (childScope) {
1960623829 childScope.$destroy();
1960723830 childScope = null;
1960823831 }
19609 if(block) {
19610 previousElements = getBlockElements(block.clone);
19611 $animate.leave(previousElements, function() {
23832 if (block) {
23833 previousElements = getBlockNodes(block.clone);
23834 $animate.leave(previousElements).then(function() {
1961223835 previousElements = null;
1961323836 });
1961423837 block = null;
1962823851 * Fetches, compiles and includes an external HTML fragment.
1962923852 *
1963023853 * By default, the template URL is restricted to the same domain and protocol as the
19631 * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
23854 * application document. This is done by calling {@link $sce#getTrustedResourceUrl
1963223855 * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols
1963323856 * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or
19634 * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link
23857 * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link
1963523858 * ng.$sce Strict Contextual Escaping}.
1963623859 *
1963723860 * In addition, the browser's
1966823891 <select ng-model="template" ng-options="t.name for t in templates">
1966923892 <option value="">(blank)</option>
1967023893 </select>
19671 url of the template: <tt>{{template.url}}</tt>
23894 url of the template: <code>{{template.url}}</code>
1967223895 <hr/>
1967323896 <div class="slide-animate-container">
1967423897 <div class="slide-animate" ng-include="template.url"></div>
1976923992 * @eventType emit on the scope ngInclude was declared in
1977023993 * @description
1977123994 * Emitted every time the ngInclude content is requested.
23995 *
23996 * @param {Object} angularEvent Synthetic event object.
23997 * @param {String} src URL of content to load.
1977223998 */
1977323999
1977424000
1977824004 * @eventType emit on the current ngInclude scope
1977924005 * @description
1978024006 * Emitted every time the ngInclude content is reloaded.
24007 *
24008 * @param {Object} angularEvent Synthetic event object.
24009 * @param {String} src URL of content to load.
1978124010 */
19782 var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce',
19783 function($http, $templateCache, $anchorScroll, $animate, $sce) {
24011
24012
24013 /**
24014 * @ngdoc event
24015 * @name ngInclude#$includeContentError
24016 * @eventType emit on the scope ngInclude was declared in
24017 * @description
24018 * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299)
24019 *
24020 * @param {Object} angularEvent Synthetic event object.
24021 * @param {String} src URL of content to load.
24022 */
24023 var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
24024 function($templateRequest, $anchorScroll, $animate) {
1978424025 return {
1978524026 restrict: 'ECA',
1978624027 priority: 400,
1979924040 currentElement;
1980024041
1980124042 var cleanupLastIncludeContent = function() {
19802 if(previousElement) {
24043 if (previousElement) {
1980324044 previousElement.remove();
1980424045 previousElement = null;
1980524046 }
19806 if(currentScope) {
24047 if (currentScope) {
1980724048 currentScope.$destroy();
1980824049 currentScope = null;
1980924050 }
19810 if(currentElement) {
19811 $animate.leave(currentElement, function() {
24051 if (currentElement) {
24052 $animate.leave(currentElement).then(function() {
1981224053 previousElement = null;
1981324054 });
1981424055 previousElement = currentElement;
1981624057 }
1981724058 };
1981824059
19819 scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) {
24060 scope.$watch(srcExp, function ngIncludeWatchAction(src) {
1982024061 var afterAnimation = function() {
1982124062 if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
1982224063 $anchorScroll();
1982524066 var thisChangeId = ++changeCounter;
1982624067
1982724068 if (src) {
19828 $http.get(src, {cache: $templateCache}).success(function(response) {
24069 //set the 2nd param to true to ignore the template request error so that the inner
24070 //contents and scope can be cleaned up.
24071 $templateRequest(src, true).then(function(response) {
1982924072 if (thisChangeId !== changeCounter) return;
1983024073 var newScope = scope.$new();
1983124074 ctrl.template = response;
1983824081 // directives to non existing elements.
1983924082 var clone = $transclude(newScope, function(clone) {
1984024083 cleanupLastIncludeContent();
19841 $animate.enter(clone, null, $element, afterAnimation);
24084 $animate.enter(clone, null, $element).then(afterAnimation);
1984224085 });
1984324086
1984424087 currentScope = newScope;
1984524088 currentElement = clone;
1984624089
19847 currentScope.$emit('$includeContentLoaded');
24090 currentScope.$emit('$includeContentLoaded', src);
1984824091 scope.$eval(onloadExp);
19849 }).error(function() {
19850 if (thisChangeId === changeCounter) cleanupLastIncludeContent();
24092 }, function() {
24093 if (thisChangeId === changeCounter) {
24094 cleanupLastIncludeContent();
24095 scope.$emit('$includeContentError', src);
24096 }
1985124097 });
19852 scope.$emit('$includeContentRequested');
24098 scope.$emit('$includeContentRequested', src);
1985324099 } else {
1985424100 cleanupLastIncludeContent();
1985524101 ctrl.template = null;
1987224118 priority: -400,
1987324119 require: 'ngInclude',
1987424120 link: function(scope, $element, $attr, ctrl) {
24121 if (/SVG/.test($element[0].toString())) {
24122 // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
24123 // support innerHTML, so detect this here and try to generate the contents
24124 // specially.
24125 $element.empty();
24126 $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
24127 function namespaceAdaptedClone(clone) {
24128 $element.append(clone);
24129 }, {futureParentElement: $element});
24130 return;
24131 }
24132
1987524133 $element.html(ctrl.template);
1987624134 $compile($element.contents())(scope);
1987724135 }
1988724145 * The `ngInit` directive allows you to evaluate an expression in the
1988824146 * current scope.
1988924147 *
19890 * <div class="alert alert-error">
24148 * <div class="alert alert-danger">
1989124149 * The only appropriate use of `ngInit` is for aliasing special properties of
1989224150 * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you
1989324151 * should use {@link guide/controller controllers} rather than `ngInit`
1989724155 * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make
1989824156 * sure you have parenthesis for correct precedence:
1989924157 * <pre class="prettyprint">
19900 * <div ng-init="test1 = (data | orderBy:'name')"></div>
24158 * `<div ng-init="test1 = (data | orderBy:'name')"></div>`
1990124159 * </pre>
1990224160 * </div>
1990324161 *
1994724205
1994824206 /**
1994924207 * @ngdoc directive
24208 * @name ngList
24209 *
24210 * @description
24211 * Text input that converts between a delimited string and an array of strings. The default
24212 * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom
24213 * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`.
24214 *
24215 * The behaviour of the directive is affected by the use of the `ngTrim` attribute.
24216 * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
24217 * list item is respected. This implies that the user of the directive is responsible for
24218 * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
24219 * tab or newline character.
24220 * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
24221 * when joining the list items back together) and whitespace around each list item is stripped
24222 * before it is added to the model.
24223 *
24224 * ### Example with Validation
24225 *
24226 * <example name="ngList-directive" module="listExample">
24227 * <file name="app.js">
24228 * angular.module('listExample', [])
24229 * .controller('ExampleController', ['$scope', function($scope) {
24230 * $scope.names = ['morpheus', 'neo', 'trinity'];
24231 * }]);
24232 * </file>
24233 * <file name="index.html">
24234 * <form name="myForm" ng-controller="ExampleController">
24235 * <label>List: <input name="namesInput" ng-model="names" ng-list required></label>
24236 * <span role="alert">
24237 * <span class="error" ng-show="myForm.namesInput.$error.required">
24238 * Required!</span>
24239 * </span>
24240 * <br>
24241 * <tt>names = {{names}}</tt><br/>
24242 * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
24243 * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
24244 * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
24245 * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
24246 * </form>
24247 * </file>
24248 * <file name="protractor.js" type="protractor">
24249 * var listInput = element(by.model('names'));
24250 * var names = element(by.exactBinding('names'));
24251 * var valid = element(by.binding('myForm.namesInput.$valid'));
24252 * var error = element(by.css('span.error'));
24253 *
24254 * it('should initialize to model', function() {
24255 * expect(names.getText()).toContain('["morpheus","neo","trinity"]');
24256 * expect(valid.getText()).toContain('true');
24257 * expect(error.getCssValue('display')).toBe('none');
24258 * });
24259 *
24260 * it('should be invalid if empty', function() {
24261 * listInput.clear();
24262 * listInput.sendKeys('');
24263 *
24264 * expect(names.getText()).toContain('');
24265 * expect(valid.getText()).toContain('false');
24266 * expect(error.getCssValue('display')).not.toBe('none');
24267 * });
24268 * </file>
24269 * </example>
24270 *
24271 * ### Example - splitting on whitespace
24272 * <example name="ngList-directive-newlines">
24273 * <file name="index.html">
24274 * <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
24275 * <pre>{{ list | json }}</pre>
24276 * </file>
24277 * <file name="protractor.js" type="protractor">
24278 * it("should split the text by newlines", function() {
24279 * var listInput = element(by.model('list'));
24280 * var output = element(by.binding('list | json'));
24281 * listInput.sendKeys('abc\ndef\nghi');
24282 * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
24283 * });
24284 * </file>
24285 * </example>
24286 *
24287 * @element input
24288 * @param {string=} ngList optional delimiter that should be used to split the value.
24289 */
24290 var ngListDirective = function() {
24291 return {
24292 restrict: 'A',
24293 priority: 100,
24294 require: 'ngModel',
24295 link: function(scope, element, attr, ctrl) {
24296 // We want to control whitespace trimming so we use this convoluted approach
24297 // to access the ngList attribute, which doesn't pre-trim the attribute
24298 var ngList = element.attr(attr.$attr.ngList) || ', ';
24299 var trimValues = attr.ngTrim !== 'false';
24300 var separator = trimValues ? trim(ngList) : ngList;
24301
24302 var parse = function(viewValue) {
24303 // If the viewValue is invalid (say required but empty) it will be `undefined`
24304 if (isUndefined(viewValue)) return;
24305
24306 var list = [];
24307
24308 if (viewValue) {
24309 forEach(viewValue.split(separator), function(value) {
24310 if (value) list.push(trimValues ? trim(value) : value);
24311 });
24312 }
24313
24314 return list;
24315 };
24316
24317 ctrl.$parsers.push(parse);
24318 ctrl.$formatters.push(function(value) {
24319 if (isArray(value)) {
24320 return value.join(ngList);
24321 }
24322
24323 return undefined;
24324 });
24325
24326 // Override the standard $isEmpty because an empty array means the input is empty.
24327 ctrl.$isEmpty = function(value) {
24328 return !value || !value.length;
24329 };
24330 }
24331 };
24332 };
24333
24334 /* global VALID_CLASS: true,
24335 INVALID_CLASS: true,
24336 PRISTINE_CLASS: true,
24337 DIRTY_CLASS: true,
24338 UNTOUCHED_CLASS: true,
24339 TOUCHED_CLASS: true,
24340 */
24341
24342 var VALID_CLASS = 'ng-valid',
24343 INVALID_CLASS = 'ng-invalid',
24344 PRISTINE_CLASS = 'ng-pristine',
24345 DIRTY_CLASS = 'ng-dirty',
24346 UNTOUCHED_CLASS = 'ng-untouched',
24347 TOUCHED_CLASS = 'ng-touched',
24348 PENDING_CLASS = 'ng-pending';
24349
24350
24351 var $ngModelMinErr = new minErr('ngModel');
24352
24353 /**
24354 * @ngdoc type
24355 * @name ngModel.NgModelController
24356 *
24357 * @property {string} $viewValue Actual string value in the view.
24358 * @property {*} $modelValue The value in the model that the control is bound to.
24359 * @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
24360 the control reads value from the DOM. The functions are called in array order, each passing
24361 its return value through to the next. The last return value is forwarded to the
24362 {@link ngModel.NgModelController#$validators `$validators`} collection.
24363
24364 Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
24365 `$viewValue`}.
24366
24367 Returning `undefined` from a parser means a parse error occurred. In that case,
24368 no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
24369 will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
24370 is set to `true`. The parse error is stored in `ngModel.$error.parse`.
24371
24372 *
24373 * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever
24374 the model value changes. The functions are called in reverse array order, each passing the value through to the
24375 next. The last return value is used as the actual DOM value.
24376 Used to format / convert values for display in the control.
24377 * ```js
24378 * function formatter(value) {
24379 * if (value) {
24380 * return value.toUpperCase();
24381 * }
24382 * }
24383 * ngModel.$formatters.push(formatter);
24384 * ```
24385 *
24386 * @property {Object.<string, function>} $validators A collection of validators that are applied
24387 * whenever the model value changes. The key value within the object refers to the name of the
24388 * validator while the function refers to the validation operation. The validation operation is
24389 * provided with the model value as an argument and must return a true or false value depending
24390 * on the response of that validation.
24391 *
24392 * ```js
24393 * ngModel.$validators.validCharacters = function(modelValue, viewValue) {
24394 * var value = modelValue || viewValue;
24395 * return /[0-9]+/.test(value) &&
24396 * /[a-z]+/.test(value) &&
24397 * /[A-Z]+/.test(value) &&
24398 * /\W+/.test(value);
24399 * };
24400 * ```
24401 *
24402 * @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to
24403 * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
24404 * is expected to return a promise when it is run during the model validation process. Once the promise
24405 * is delivered then the validation status will be set to true when fulfilled and false when rejected.
24406 * When the asynchronous validators are triggered, each of the validators will run in parallel and the model
24407 * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator
24408 * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators
24409 * will only run once all synchronous validators have passed.
24410 *
24411 * Please note that if $http is used then it is important that the server returns a success HTTP response code
24412 * in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
24413 *
24414 * ```js
24415 * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
24416 * var value = modelValue || viewValue;
24417 *
24418 * // Lookup user by username
24419 * return $http.get('/api/users/' + value).
24420 * then(function resolved() {
24421 * //username exists, this means validation fails
24422 * return $q.reject('exists');
24423 * }, function rejected() {
24424 * //username does not exist, therefore this validation passes
24425 * return true;
24426 * });
24427 * };
24428 * ```
24429 *
24430 * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
24431 * view value has changed. It is called with no arguments, and its return value is ignored.
24432 * This can be used in place of additional $watches against the model value.
24433 *
24434 * @property {Object} $error An object hash with all failing validator ids as keys.
24435 * @property {Object} $pending An object hash with all pending validator ids as keys.
24436 *
24437 * @property {boolean} $untouched True if control has not lost focus yet.
24438 * @property {boolean} $touched True if control has lost focus.
24439 * @property {boolean} $pristine True if user has not interacted with the control yet.
24440 * @property {boolean} $dirty True if user has already interacted with the control.
24441 * @property {boolean} $valid True if there is no error.
24442 * @property {boolean} $invalid True if at least one error on the control.
24443 * @property {string} $name The name attribute of the control.
24444 *
24445 * @description
24446 *
24447 * `NgModelController` provides API for the {@link ngModel `ngModel`} directive.
24448 * The controller contains services for data-binding, validation, CSS updates, and value formatting
24449 * and parsing. It purposefully does not contain any logic which deals with DOM rendering or
24450 * listening to DOM events.
24451 * Such DOM related logic should be provided by other directives which make use of
24452 * `NgModelController` for data-binding to control elements.
24453 * Angular provides this DOM logic for most {@link input `input`} elements.
24454 * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example
24455 * custom control example} that uses `ngModelController` to bind to `contenteditable` elements.
24456 *
24457 * @example
24458 * ### Custom Control Example
24459 * This example shows how to use `NgModelController` with a custom control to achieve
24460 * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
24461 * collaborate together to achieve the desired result.
24462 *
24463 * `contenteditable` is an HTML5 attribute, which tells the browser to let the element
24464 * contents be edited in place by the user.
24465 *
24466 * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize}
24467 * module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`).
24468 * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks
24469 * that content using the `$sce` service.
24470 *
24471 * <example name="NgModelController" module="customControl" deps="angular-sanitize.js">
24472 <file name="style.css">
24473 [contenteditable] {
24474 border: 1px solid black;
24475 background-color: white;
24476 min-height: 20px;
24477 }
24478
24479 .ng-invalid {
24480 border: 1px solid red;
24481 }
24482
24483 </file>
24484 <file name="script.js">
24485 angular.module('customControl', ['ngSanitize']).
24486 directive('contenteditable', ['$sce', function($sce) {
24487 return {
24488 restrict: 'A', // only activate on element attribute
24489 require: '?ngModel', // get a hold of NgModelController
24490 link: function(scope, element, attrs, ngModel) {
24491 if (!ngModel) return; // do nothing if no ng-model
24492
24493 // Specify how UI should be updated
24494 ngModel.$render = function() {
24495 element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
24496 };
24497
24498 // Listen for change events to enable binding
24499 element.on('blur keyup change', function() {
24500 scope.$evalAsync(read);
24501 });
24502 read(); // initialize
24503
24504 // Write data to the model
24505 function read() {
24506 var html = element.html();
24507 // When we clear the content editable the browser leaves a <br> behind
24508 // If strip-br attribute is provided then we strip this out
24509 if ( attrs.stripBr && html == '<br>' ) {
24510 html = '';
24511 }
24512 ngModel.$setViewValue(html);
24513 }
24514 }
24515 };
24516 }]);
24517 </file>
24518 <file name="index.html">
24519 <form name="myForm">
24520 <div contenteditable
24521 name="myWidget" ng-model="userContent"
24522 strip-br="true"
24523 required>Change me!</div>
24524 <span ng-show="myForm.myWidget.$error.required">Required!</span>
24525 <hr>
24526 <textarea ng-model="userContent" aria-label="Dynamic textarea"></textarea>
24527 </form>
24528 </file>
24529 <file name="protractor.js" type="protractor">
24530 it('should data-bind and become invalid', function() {
24531 if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
24532 // SafariDriver can't handle contenteditable
24533 // and Firefox driver can't clear contenteditables very well
24534 return;
24535 }
24536 var contentEditable = element(by.css('[contenteditable]'));
24537 var content = 'Change me!';
24538
24539 expect(contentEditable.getText()).toEqual(content);
24540
24541 contentEditable.clear();
24542 contentEditable.sendKeys(protractor.Key.BACK_SPACE);
24543 expect(contentEditable.getText()).toEqual('');
24544 expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
24545 });
24546 </file>
24547 * </example>
24548 *
24549 *
24550 */
24551 var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
24552 function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
24553 this.$viewValue = Number.NaN;
24554 this.$modelValue = Number.NaN;
24555 this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
24556 this.$validators = {};
24557 this.$asyncValidators = {};
24558 this.$parsers = [];
24559 this.$formatters = [];
24560 this.$viewChangeListeners = [];
24561 this.$untouched = true;
24562 this.$touched = false;
24563 this.$pristine = true;
24564 this.$dirty = false;
24565 this.$valid = true;
24566 this.$invalid = false;
24567 this.$error = {}; // keep invalid keys here
24568 this.$$success = {}; // keep valid keys here
24569 this.$pending = undefined; // keep pending keys here
24570 this.$name = $interpolate($attr.name || '', false)($scope);
24571
24572
24573 var parsedNgModel = $parse($attr.ngModel),
24574 parsedNgModelAssign = parsedNgModel.assign,
24575 ngModelGet = parsedNgModel,
24576 ngModelSet = parsedNgModelAssign,
24577 pendingDebounce = null,
24578 parserValid,
24579 ctrl = this;
24580
24581 this.$$setOptions = function(options) {
24582 ctrl.$options = options;
24583 if (options && options.getterSetter) {
24584 var invokeModelGetter = $parse($attr.ngModel + '()'),
24585 invokeModelSetter = $parse($attr.ngModel + '($$$p)');
24586
24587 ngModelGet = function($scope) {
24588 var modelValue = parsedNgModel($scope);
24589 if (isFunction(modelValue)) {
24590 modelValue = invokeModelGetter($scope);
24591 }
24592 return modelValue;
24593 };
24594 ngModelSet = function($scope, newValue) {
24595 if (isFunction(parsedNgModel($scope))) {
24596 invokeModelSetter($scope, {$$$p: ctrl.$modelValue});
24597 } else {
24598 parsedNgModelAssign($scope, ctrl.$modelValue);
24599 }
24600 };
24601 } else if (!parsedNgModel.assign) {
24602 throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
24603 $attr.ngModel, startingTag($element));
24604 }
24605 };
24606
24607 /**
24608 * @ngdoc method
24609 * @name ngModel.NgModelController#$render
24610 *
24611 * @description
24612 * Called when the view needs to be updated. It is expected that the user of the ng-model
24613 * directive will implement this method.
24614 *
24615 * The `$render()` method is invoked in the following situations:
24616 *
24617 * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last
24618 * committed value then `$render()` is called to update the input control.
24619 * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and
24620 * the `$viewValue` are different from last time.
24621 *
24622 * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
24623 * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue`
24624 * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
24625 * invoked if you only change a property on the objects.
24626 */
24627 this.$render = noop;
24628
24629 /**
24630 * @ngdoc method
24631 * @name ngModel.NgModelController#$isEmpty
24632 *
24633 * @description
24634 * This is called when we need to determine if the value of an input is empty.
24635 *
24636 * For instance, the required directive does this to work out if the input has data or not.
24637 *
24638 * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
24639 *
24640 * You can override this for input directives whose concept of being empty is different from the
24641 * default. The `checkboxInputType` directive does this because in its case a value of `false`
24642 * implies empty.
24643 *
24644 * @param {*} value The value of the input to check for emptiness.
24645 * @returns {boolean} True if `value` is "empty".
24646 */
24647 this.$isEmpty = function(value) {
24648 return isUndefined(value) || value === '' || value === null || value !== value;
24649 };
24650
24651 var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
24652 currentValidationRunId = 0;
24653
24654 /**
24655 * @ngdoc method
24656 * @name ngModel.NgModelController#$setValidity
24657 *
24658 * @description
24659 * Change the validity state, and notify the form.
24660 *
24661 * This method can be called within $parsers/$formatters or a custom validation implementation.
24662 * However, in most cases it should be sufficient to use the `ngModel.$validators` and
24663 * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
24664 *
24665 * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
24666 * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
24667 * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
24668 * The `validationErrorKey` should be in camelCase and will get converted into dash-case
24669 * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
24670 * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
24671 * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
24672 * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
24673 * Skipped is used by Angular when validators do not run because of parse errors and
24674 * when `$asyncValidators` do not run because any of the `$validators` failed.
24675 */
24676 addSetValidityMethod({
24677 ctrl: this,
24678 $element: $element,
24679 set: function(object, property) {
24680 object[property] = true;
24681 },
24682 unset: function(object, property) {
24683 delete object[property];
24684 },
24685 parentForm: parentForm,
24686 $animate: $animate
24687 });
24688
24689 /**
24690 * @ngdoc method
24691 * @name ngModel.NgModelController#$setPristine
24692 *
24693 * @description
24694 * Sets the control to its pristine state.
24695 *
24696 * This method can be called to remove the `ng-dirty` class and set the control to its pristine
24697 * state (`ng-pristine` class). A model is considered to be pristine when the control
24698 * has not been changed from when first compiled.
24699 */
24700 this.$setPristine = function() {
24701 ctrl.$dirty = false;
24702 ctrl.$pristine = true;
24703 $animate.removeClass($element, DIRTY_CLASS);
24704 $animate.addClass($element, PRISTINE_CLASS);
24705 };
24706
24707 /**
24708 * @ngdoc method
24709 * @name ngModel.NgModelController#$setDirty
24710 *
24711 * @description
24712 * Sets the control to its dirty state.
24713 *
24714 * This method can be called to remove the `ng-pristine` class and set the control to its dirty
24715 * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
24716 * from when first compiled.
24717 */
24718 this.$setDirty = function() {
24719 ctrl.$dirty = true;
24720 ctrl.$pristine = false;
24721 $animate.removeClass($element, PRISTINE_CLASS);
24722 $animate.addClass($element, DIRTY_CLASS);
24723 parentForm.$setDirty();
24724 };
24725
24726 /**
24727 * @ngdoc method
24728 * @name ngModel.NgModelController#$setUntouched
24729 *
24730 * @description
24731 * Sets the control to its untouched state.
24732 *
24733 * This method can be called to remove the `ng-touched` class and set the control to its
24734 * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched
24735 * by default, however this function can be used to restore that state if the model has
24736 * already been touched by the user.
24737 */
24738 this.$setUntouched = function() {
24739 ctrl.$touched = false;
24740 ctrl.$untouched = true;
24741 $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
24742 };
24743
24744 /**
24745 * @ngdoc method
24746 * @name ngModel.NgModelController#$setTouched
24747 *
24748 * @description
24749 * Sets the control to its touched state.
24750 *
24751 * This method can be called to remove the `ng-untouched` class and set the control to its
24752 * touched state (`ng-touched` class). A model is considered to be touched when the user has
24753 * first focused the control element and then shifted focus away from the control (blur event).
24754 */
24755 this.$setTouched = function() {
24756 ctrl.$touched = true;
24757 ctrl.$untouched = false;
24758 $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
24759 };
24760
24761 /**
24762 * @ngdoc method
24763 * @name ngModel.NgModelController#$rollbackViewValue
24764 *
24765 * @description
24766 * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
24767 * which may be caused by a pending debounced event or because the input is waiting for a some
24768 * future event.
24769 *
24770 * If you have an input that uses `ng-model-options` to set up debounced events or events such
24771 * as blur you can have a situation where there is a period when the `$viewValue`
24772 * is out of synch with the ngModel's `$modelValue`.
24773 *
24774 * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue`
24775 * programmatically before these debounced/future events have resolved/occurred, because Angular's
24776 * dirty checking mechanism is not able to tell whether the model has actually changed or not.
24777 *
24778 * The `$rollbackViewValue()` method should be called before programmatically changing the model of an
24779 * input which may have such events pending. This is important in order to make sure that the
24780 * input field will be updated with the new model value and any pending operations are cancelled.
24781 *
24782 * <example name="ng-model-cancel-update" module="cancel-update-example">
24783 * <file name="app.js">
24784 * angular.module('cancel-update-example', [])
24785 *
24786 * .controller('CancelUpdateController', ['$scope', function($scope) {
24787 * $scope.resetWithCancel = function(e) {
24788 * if (e.keyCode == 27) {
24789 * $scope.myForm.myInput1.$rollbackViewValue();
24790 * $scope.myValue = '';
24791 * }
24792 * };
24793 * $scope.resetWithoutCancel = function(e) {
24794 * if (e.keyCode == 27) {
24795 * $scope.myValue = '';
24796 * }
24797 * };
24798 * }]);
24799 * </file>
24800 * <file name="index.html">
24801 * <div ng-controller="CancelUpdateController">
24802 * <p>Try typing something in each input. See that the model only updates when you
24803 * blur off the input.
24804 * </p>
24805 * <p>Now see what happens if you start typing then press the Escape key</p>
24806 *
24807 * <form name="myForm" ng-model-options="{ updateOn: 'blur' }">
24808 * <p id="inputDescription1">With $rollbackViewValue()</p>
24809 * <input name="myInput1" aria-describedby="inputDescription1" ng-model="myValue"
24810 * ng-keydown="resetWithCancel($event)"><br/>
24811 * myValue: "{{ myValue }}"
24812 *
24813 * <p id="inputDescription2">Without $rollbackViewValue()</p>
24814 * <input name="myInput2" aria-describedby="inputDescription2" ng-model="myValue"
24815 * ng-keydown="resetWithoutCancel($event)"><br/>
24816 * myValue: "{{ myValue }}"
24817 * </form>
24818 * </div>
24819 * </file>
24820 * </example>
24821 */
24822 this.$rollbackViewValue = function() {
24823 $timeout.cancel(pendingDebounce);
24824 ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
24825 ctrl.$render();
24826 };
24827
24828 /**
24829 * @ngdoc method
24830 * @name ngModel.NgModelController#$validate
24831 *
24832 * @description
24833 * Runs each of the registered validators (first synchronous validators and then
24834 * asynchronous validators).
24835 * If the validity changes to invalid, the model will be set to `undefined`,
24836 * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`.
24837 * If the validity changes to valid, it will set the model to the last available valid
24838 * `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
24839 */
24840 this.$validate = function() {
24841 // ignore $validate before model is initialized
24842 if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
24843 return;
24844 }
24845
24846 var viewValue = ctrl.$$lastCommittedViewValue;
24847 // Note: we use the $$rawModelValue as $modelValue might have been
24848 // set to undefined during a view -> model update that found validation
24849 // errors. We can't parse the view here, since that could change
24850 // the model although neither viewValue nor the model on the scope changed
24851 var modelValue = ctrl.$$rawModelValue;
24852
24853 var prevValid = ctrl.$valid;
24854 var prevModelValue = ctrl.$modelValue;
24855
24856 var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
24857
24858 ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
24859 // If there was no change in validity, don't update the model
24860 // This prevents changing an invalid modelValue to undefined
24861 if (!allowInvalid && prevValid !== allValid) {
24862 // Note: Don't check ctrl.$valid here, as we could have
24863 // external validators (e.g. calculated on the server),
24864 // that just call $setValidity and need the model value
24865 // to calculate their validity.
24866 ctrl.$modelValue = allValid ? modelValue : undefined;
24867
24868 if (ctrl.$modelValue !== prevModelValue) {
24869 ctrl.$$writeModelToScope();
24870 }
24871 }
24872 });
24873
24874 };
24875
24876 this.$$runValidators = function(modelValue, viewValue, doneCallback) {
24877 currentValidationRunId++;
24878 var localValidationRunId = currentValidationRunId;
24879
24880 // check parser error
24881 if (!processParseErrors()) {
24882 validationDone(false);
24883 return;
24884 }
24885 if (!processSyncValidators()) {
24886 validationDone(false);
24887 return;
24888 }
24889 processAsyncValidators();
24890
24891 function processParseErrors() {
24892 var errorKey = ctrl.$$parserName || 'parse';
24893 if (parserValid === undefined) {
24894 setValidity(errorKey, null);
24895 } else {
24896 if (!parserValid) {
24897 forEach(ctrl.$validators, function(v, name) {
24898 setValidity(name, null);
24899 });
24900 forEach(ctrl.$asyncValidators, function(v, name) {
24901 setValidity(name, null);
24902 });
24903 }
24904 // Set the parse error last, to prevent unsetting it, should a $validators key == parserName
24905 setValidity(errorKey, parserValid);
24906 return parserValid;
24907 }
24908 return true;
24909 }
24910
24911 function processSyncValidators() {
24912 var syncValidatorsValid = true;
24913 forEach(ctrl.$validators, function(validator, name) {
24914 var result = validator(modelValue, viewValue);
24915 syncValidatorsValid = syncValidatorsValid && result;
24916 setValidity(name, result);
24917 });
24918 if (!syncValidatorsValid) {
24919 forEach(ctrl.$asyncValidators, function(v, name) {
24920 setValidity(name, null);
24921 });
24922 return false;
24923 }
24924 return true;
24925 }
24926
24927 function processAsyncValidators() {
24928 var validatorPromises = [];
24929 var allValid = true;
24930 forEach(ctrl.$asyncValidators, function(validator, name) {
24931 var promise = validator(modelValue, viewValue);
24932 if (!isPromiseLike(promise)) {
24933 throw $ngModelMinErr("$asyncValidators",
24934 "Expected asynchronous validator to return a promise but got '{0}' instead.", promise);
24935 }
24936 setValidity(name, undefined);
24937 validatorPromises.push(promise.then(function() {
24938 setValidity(name, true);
24939 }, function(error) {
24940 allValid = false;
24941 setValidity(name, false);
24942 }));
24943 });
24944 if (!validatorPromises.length) {
24945 validationDone(true);
24946 } else {
24947 $q.all(validatorPromises).then(function() {
24948 validationDone(allValid);
24949 }, noop);
24950 }
24951 }
24952
24953 function setValidity(name, isValid) {
24954 if (localValidationRunId === currentValidationRunId) {
24955 ctrl.$setValidity(name, isValid);
24956 }
24957 }
24958
24959 function validationDone(allValid) {
24960 if (localValidationRunId === currentValidationRunId) {
24961
24962 doneCallback(allValid);
24963 }
24964 }
24965 };
24966
24967 /**
24968 * @ngdoc method
24969 * @name ngModel.NgModelController#$commitViewValue
24970 *
24971 * @description
24972 * Commit a pending update to the `$modelValue`.
24973 *
24974 * Updates may be pending by a debounced event or because the input is waiting for a some future
24975 * event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
24976 * usually handles calling this in response to input events.
24977 */
24978 this.$commitViewValue = function() {
24979 var viewValue = ctrl.$viewValue;
24980
24981 $timeout.cancel(pendingDebounce);
24982
24983 // If the view value has not changed then we should just exit, except in the case where there is
24984 // a native validator on the element. In this case the validation state may have changed even though
24985 // the viewValue has stayed empty.
24986 if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
24987 return;
24988 }
24989 ctrl.$$lastCommittedViewValue = viewValue;
24990
24991 // change to dirty
24992 if (ctrl.$pristine) {
24993 this.$setDirty();
24994 }
24995 this.$$parseAndValidate();
24996 };
24997
24998 this.$$parseAndValidate = function() {
24999 var viewValue = ctrl.$$lastCommittedViewValue;
25000 var modelValue = viewValue;
25001 parserValid = isUndefined(modelValue) ? undefined : true;
25002
25003 if (parserValid) {
25004 for (var i = 0; i < ctrl.$parsers.length; i++) {
25005 modelValue = ctrl.$parsers[i](modelValue);
25006 if (isUndefined(modelValue)) {
25007 parserValid = false;
25008 break;
25009 }
25010 }
25011 }
25012 if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
25013 // ctrl.$modelValue has not been touched yet...
25014 ctrl.$modelValue = ngModelGet($scope);
25015 }
25016 var prevModelValue = ctrl.$modelValue;
25017 var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
25018 ctrl.$$rawModelValue = modelValue;
25019
25020 if (allowInvalid) {
25021 ctrl.$modelValue = modelValue;
25022 writeToModelIfNeeded();
25023 }
25024
25025 // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
25026 // This can happen if e.g. $setViewValue is called from inside a parser
25027 ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
25028 if (!allowInvalid) {
25029 // Note: Don't check ctrl.$valid here, as we could have
25030 // external validators (e.g. calculated on the server),
25031 // that just call $setValidity and need the model value
25032 // to calculate their validity.
25033 ctrl.$modelValue = allValid ? modelValue : undefined;
25034 writeToModelIfNeeded();
25035 }
25036 });
25037
25038 function writeToModelIfNeeded() {
25039 if (ctrl.$modelValue !== prevModelValue) {
25040 ctrl.$$writeModelToScope();
25041 }
25042 }
25043 };
25044
25045 this.$$writeModelToScope = function() {
25046 ngModelSet($scope, ctrl.$modelValue);
25047 forEach(ctrl.$viewChangeListeners, function(listener) {
25048 try {
25049 listener();
25050 } catch (e) {
25051 $exceptionHandler(e);
25052 }
25053 });
25054 };
25055
25056 /**
25057 * @ngdoc method
25058 * @name ngModel.NgModelController#$setViewValue
25059 *
25060 * @description
25061 * Update the view value.
25062 *
25063 * This method should be called when an input directive want to change the view value; typically,
25064 * this is done from within a DOM event handler.
25065 *
25066 * For example {@link ng.directive:input input} calls it when the value of the input changes and
25067 * {@link ng.directive:select select} calls it when an option is selected.
25068 *
25069 * If the new `value` is an object (rather than a string or a number), we should make a copy of the
25070 * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep
25071 * watch of objects, it only looks for a change of identity. If you only change the property of
25072 * the object then ngModel will not realise that the object has changed and will not invoke the
25073 * `$parsers` and `$validators` pipelines.
25074 *
25075 * For this reason, you should not change properties of the copy once it has been passed to
25076 * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly.
25077 *
25078 * When this method is called, the new `value` will be staged for committing through the `$parsers`
25079 * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
25080 * value sent directly for processing, finally to be applied to `$modelValue` and then the
25081 * **expression** specified in the `ng-model` attribute.
25082 *
25083 * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
25084 *
25085 * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
25086 * and the `default` trigger is not listed, all those actions will remain pending until one of the
25087 * `updateOn` events is triggered on the DOM element.
25088 * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
25089 * directive is used with a custom debounce for this particular event.
25090 *
25091 * Note that calling this function does not trigger a `$digest`.
25092 *
25093 * @param {string} value Value from the view.
25094 * @param {string} trigger Event that triggered the update.
25095 */
25096 this.$setViewValue = function(value, trigger) {
25097 ctrl.$viewValue = value;
25098 if (!ctrl.$options || ctrl.$options.updateOnDefault) {
25099 ctrl.$$debounceViewValueCommit(trigger);
25100 }
25101 };
25102
25103 this.$$debounceViewValueCommit = function(trigger) {
25104 var debounceDelay = 0,
25105 options = ctrl.$options,
25106 debounce;
25107
25108 if (options && isDefined(options.debounce)) {
25109 debounce = options.debounce;
25110 if (isNumber(debounce)) {
25111 debounceDelay = debounce;
25112 } else if (isNumber(debounce[trigger])) {
25113 debounceDelay = debounce[trigger];
25114 } else if (isNumber(debounce['default'])) {
25115 debounceDelay = debounce['default'];
25116 }
25117 }
25118
25119 $timeout.cancel(pendingDebounce);
25120 if (debounceDelay) {
25121 pendingDebounce = $timeout(function() {
25122 ctrl.$commitViewValue();
25123 }, debounceDelay);
25124 } else if ($rootScope.$$phase) {
25125 ctrl.$commitViewValue();
25126 } else {
25127 $scope.$apply(function() {
25128 ctrl.$commitViewValue();
25129 });
25130 }
25131 };
25132
25133 // model -> value
25134 // Note: we cannot use a normal scope.$watch as we want to detect the following:
25135 // 1. scope value is 'a'
25136 // 2. user enters 'b'
25137 // 3. ng-change kicks in and reverts scope value to 'a'
25138 // -> scope value did not change since the last digest as
25139 // ng-change executes in apply phase
25140 // 4. view should be changed back to 'a'
25141 $scope.$watch(function ngModelWatch() {
25142 var modelValue = ngModelGet($scope);
25143
25144 // if scope model value and ngModel value are out of sync
25145 // TODO(perf): why not move this to the action fn?
25146 if (modelValue !== ctrl.$modelValue &&
25147 // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
25148 (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
25149 ) {
25150 ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
25151 parserValid = undefined;
25152
25153 var formatters = ctrl.$formatters,
25154 idx = formatters.length;
25155
25156 var viewValue = modelValue;
25157 while (idx--) {
25158 viewValue = formatters[idx](viewValue);
25159 }
25160 if (ctrl.$viewValue !== viewValue) {
25161 ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
25162 ctrl.$render();
25163
25164 ctrl.$$runValidators(modelValue, viewValue, noop);
25165 }
25166 }
25167
25168 return modelValue;
25169 });
25170 }];
25171
25172
25173 /**
25174 * @ngdoc directive
25175 * @name ngModel
25176 *
25177 * @element input
25178 * @priority 1
25179 *
25180 * @description
25181 * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
25182 * property on the scope using {@link ngModel.NgModelController NgModelController},
25183 * which is created and exposed by this directive.
25184 *
25185 * `ngModel` is responsible for:
25186 *
25187 * - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
25188 * require.
25189 * - Providing validation behavior (i.e. required, number, email, url).
25190 * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
25191 * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations.
25192 * - Registering the control with its parent {@link ng.directive:form form}.
25193 *
25194 * Note: `ngModel` will try to bind to the property given by evaluating the expression on the
25195 * current scope. If the property doesn't already exist on this scope, it will be created
25196 * implicitly and added to the scope.
25197 *
25198 * For best practices on using `ngModel`, see:
25199 *
25200 * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes)
25201 *
25202 * For basic examples, how to use `ngModel`, see:
25203 *
25204 * - {@link ng.directive:input input}
25205 * - {@link input[text] text}
25206 * - {@link input[checkbox] checkbox}
25207 * - {@link input[radio] radio}
25208 * - {@link input[number] number}
25209 * - {@link input[email] email}
25210 * - {@link input[url] url}
25211 * - {@link input[date] date}
25212 * - {@link input[datetime-local] datetime-local}
25213 * - {@link input[time] time}
25214 * - {@link input[month] month}
25215 * - {@link input[week] week}
25216 * - {@link ng.directive:select select}
25217 * - {@link ng.directive:textarea textarea}
25218 *
25219 * # CSS classes
25220 * The following CSS classes are added and removed on the associated input/select/textarea element
25221 * depending on the validity of the model.
25222 *
25223 * - `ng-valid`: the model is valid
25224 * - `ng-invalid`: the model is invalid
25225 * - `ng-valid-[key]`: for each valid key added by `$setValidity`
25226 * - `ng-invalid-[key]`: for each invalid key added by `$setValidity`
25227 * - `ng-pristine`: the control hasn't been interacted with yet
25228 * - `ng-dirty`: the control has been interacted with
25229 * - `ng-touched`: the control has been blurred
25230 * - `ng-untouched`: the control hasn't been blurred
25231 * - `ng-pending`: any `$asyncValidators` are unfulfilled
25232 *
25233 * Keep in mind that ngAnimate can detect each of these classes when added and removed.
25234 *
25235 * ## Animation Hooks
25236 *
25237 * Animations within models are triggered when any of the associated CSS classes are added and removed
25238 * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
25239 * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
25240 * The animations that are triggered within ngModel are similar to how they work in ngClass and
25241 * animations can be hooked into using CSS transitions, keyframes as well as JS animations.
25242 *
25243 * The following example shows a simple way to utilize CSS transitions to style an input element
25244 * that has been rendered as invalid after it has been validated:
25245 *
25246 * <pre>
25247 * //be sure to include ngAnimate as a module to hook into more
25248 * //advanced animations
25249 * .my-input {
25250 * transition:0.5s linear all;
25251 * background: white;
25252 * }
25253 * .my-input.ng-invalid {
25254 * background: red;
25255 * color:white;
25256 * }
25257 * </pre>
25258 *
25259 * @example
25260 * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample">
25261 <file name="index.html">
25262 <script>
25263 angular.module('inputExample', [])
25264 .controller('ExampleController', ['$scope', function($scope) {
25265 $scope.val = '1';
25266 }]);
25267 </script>
25268 <style>
25269 .my-input {
25270 -webkit-transition:all linear 0.5s;
25271 transition:all linear 0.5s;
25272 background: transparent;
25273 }
25274 .my-input.ng-invalid {
25275 color:white;
25276 background: red;
25277 }
25278 </style>
25279 <p id="inputDescription">
25280 Update input to see transitions when valid/invalid.
25281 Integer is a valid value.
25282 </p>
25283 <form name="testForm" ng-controller="ExampleController">
25284 <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input"
25285 aria-describedby="inputDescription" />
25286 </form>
25287 </file>
25288 * </example>
25289 *
25290 * ## Binding to a getter/setter
25291 *
25292 * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
25293 * function that returns a representation of the model when called with zero arguments, and sets
25294 * the internal state of a model when called with an argument. It's sometimes useful to use this
25295 * for models that have an internal representation that's different from what the model exposes
25296 * to the view.
25297 *
25298 * <div class="alert alert-success">
25299 * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more
25300 * frequently than other parts of your code.
25301 * </div>
25302 *
25303 * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that
25304 * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to
25305 * a `<form>`, which will enable this behavior for all `<input>`s within it. See
25306 * {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
25307 *
25308 * The following example shows how to use `ngModel` with a getter/setter:
25309 *
25310 * @example
25311 * <example name="ngModel-getter-setter" module="getterSetterExample">
25312 <file name="index.html">
25313 <div ng-controller="ExampleController">
25314 <form name="userForm">
25315 <label>Name:
25316 <input type="text" name="userName"
25317 ng-model="user.name"
25318 ng-model-options="{ getterSetter: true }" />
25319 </label>
25320 </form>
25321 <pre>user.name = <span ng-bind="user.name()"></span></pre>
25322 </div>
25323 </file>
25324 <file name="app.js">
25325 angular.module('getterSetterExample', [])
25326 .controller('ExampleController', ['$scope', function($scope) {
25327 var _name = 'Brian';
25328 $scope.user = {
25329 name: function(newName) {
25330 // Note that newName can be undefined for two reasons:
25331 // 1. Because it is called as a getter and thus called with no arguments
25332 // 2. Because the property should actually be set to undefined. This happens e.g. if the
25333 // input is invalid
25334 return arguments.length ? (_name = newName) : _name;
25335 }
25336 };
25337 }]);
25338 </file>
25339 * </example>
25340 */
25341 var ngModelDirective = ['$rootScope', function($rootScope) {
25342 return {
25343 restrict: 'A',
25344 require: ['ngModel', '^?form', '^?ngModelOptions'],
25345 controller: NgModelController,
25346 // Prelink needs to run before any input directive
25347 // so that we can set the NgModelOptions in NgModelController
25348 // before anyone else uses it.
25349 priority: 1,
25350 compile: function ngModelCompile(element) {
25351 // Setup initial state of the control
25352 element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS);
25353
25354 return {
25355 pre: function ngModelPreLink(scope, element, attr, ctrls) {
25356 var modelCtrl = ctrls[0],
25357 formCtrl = ctrls[1] || nullFormCtrl;
25358
25359 modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
25360
25361 // notify others, especially parent forms
25362 formCtrl.$addControl(modelCtrl);
25363
25364 attr.$observe('name', function(newValue) {
25365 if (modelCtrl.$name !== newValue) {
25366 formCtrl.$$renameControl(modelCtrl, newValue);
25367 }
25368 });
25369
25370 scope.$on('$destroy', function() {
25371 formCtrl.$removeControl(modelCtrl);
25372 });
25373 },
25374 post: function ngModelPostLink(scope, element, attr, ctrls) {
25375 var modelCtrl = ctrls[0];
25376 if (modelCtrl.$options && modelCtrl.$options.updateOn) {
25377 element.on(modelCtrl.$options.updateOn, function(ev) {
25378 modelCtrl.$$debounceViewValueCommit(ev && ev.type);
25379 });
25380 }
25381
25382 element.on('blur', function(ev) {
25383 if (modelCtrl.$touched) return;
25384
25385 if ($rootScope.$$phase) {
25386 scope.$evalAsync(modelCtrl.$setTouched);
25387 } else {
25388 scope.$apply(modelCtrl.$setTouched);
25389 }
25390 });
25391 }
25392 };
25393 }
25394 };
25395 }];
25396
25397 var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
25398
25399 /**
25400 * @ngdoc directive
25401 * @name ngModelOptions
25402 *
25403 * @description
25404 * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
25405 * events that will trigger a model update and/or a debouncing delay so that the actual update only
25406 * takes place when a timer expires; this timer will be reset after another change takes place.
25407 *
25408 * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
25409 * be different from the value in the actual model. This means that if you update the model you
25410 * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
25411 * order to make sure it is synchronized with the model and that any debounced action is canceled.
25412 *
25413 * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
25414 * method is by making sure the input is placed inside a form that has a `name` attribute. This is
25415 * important because `form` controllers are published to the related scope under the name in their
25416 * `name` attribute.
25417 *
25418 * Any pending changes will take place immediately when an enclosing form is submitted via the
25419 * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
25420 * to have access to the updated model.
25421 *
25422 * `ngModelOptions` has an effect on the element it's declared on and its descendants.
25423 *
25424 * @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
25425 * - `updateOn`: string specifying which event should the input be bound to. You can set several
25426 * events using an space delimited list. There is a special event called `default` that
25427 * matches the default events belonging of the control.
25428 * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
25429 * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
25430 * custom value for each event. For example:
25431 * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
25432 * - `allowInvalid`: boolean value which indicates that the model can be set with values that did
25433 * not validate correctly instead of the default behavior of setting the model to undefined.
25434 * - `getterSetter`: boolean value which determines whether or not to treat functions bound to
25435 `ngModel` as getters/setters.
25436 * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
25437 * `<input type="date">`, `<input type="time">`, ... . It understands UTC/GMT and the
25438 * continental US time zone abbreviations, but for general use, use a time zone offset, for
25439 * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
25440 * If not specified, the timezone of the browser will be used.
25441 *
25442 * @example
25443
25444 The following example shows how to override immediate updates. Changes on the inputs within the
25445 form will update the model only when the control loses focus (blur event). If `escape` key is
25446 pressed while the input field is focused, the value is reset to the value in the current model.
25447
25448 <example name="ngModelOptions-directive-blur" module="optionsExample">
25449 <file name="index.html">
25450 <div ng-controller="ExampleController">
25451 <form name="userForm">
25452 <label>Name:
25453 <input type="text" name="userName"
25454 ng-model="user.name"
25455 ng-model-options="{ updateOn: 'blur' }"
25456 ng-keyup="cancel($event)" />
25457 </label><br />
25458 <label>Other data:
25459 <input type="text" ng-model="user.data" />
25460 </label><br />
25461 </form>
25462 <pre>user.name = <span ng-bind="user.name"></span></pre>
25463 </div>
25464 </file>
25465 <file name="app.js">
25466 angular.module('optionsExample', [])
25467 .controller('ExampleController', ['$scope', function($scope) {
25468 $scope.user = { name: 'say', data: '' };
25469
25470 $scope.cancel = function(e) {
25471 if (e.keyCode == 27) {
25472 $scope.userForm.userName.$rollbackViewValue();
25473 }
25474 };
25475 }]);
25476 </file>
25477 <file name="protractor.js" type="protractor">
25478 var model = element(by.binding('user.name'));
25479 var input = element(by.model('user.name'));
25480 var other = element(by.model('user.data'));
25481
25482 it('should allow custom events', function() {
25483 input.sendKeys(' hello');
25484 input.click();
25485 expect(model.getText()).toEqual('say');
25486 other.click();
25487 expect(model.getText()).toEqual('say hello');
25488 });
25489
25490 it('should $rollbackViewValue when model changes', function() {
25491 input.sendKeys(' hello');
25492 expect(input.getAttribute('value')).toEqual('say hello');
25493 input.sendKeys(protractor.Key.ESCAPE);
25494 expect(input.getAttribute('value')).toEqual('say');
25495 other.click();
25496 expect(model.getText()).toEqual('say');
25497 });
25498 </file>
25499 </example>
25500
25501 This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
25502 If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
25503
25504 <example name="ngModelOptions-directive-debounce" module="optionsExample">
25505 <file name="index.html">
25506 <div ng-controller="ExampleController">
25507 <form name="userForm">
25508 <label>Name:
25509 <input type="text" name="userName"
25510 ng-model="user.name"
25511 ng-model-options="{ debounce: 1000 }" />
25512 </label>
25513 <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button>
25514 <br />
25515 </form>
25516 <pre>user.name = <span ng-bind="user.name"></span></pre>
25517 </div>
25518 </file>
25519 <file name="app.js">
25520 angular.module('optionsExample', [])
25521 .controller('ExampleController', ['$scope', function($scope) {
25522 $scope.user = { name: 'say' };
25523 }]);
25524 </file>
25525 </example>
25526
25527 This one shows how to bind to getter/setters:
25528
25529 <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
25530 <file name="index.html">
25531 <div ng-controller="ExampleController">
25532 <form name="userForm">
25533 <label>Name:
25534 <input type="text" name="userName"
25535 ng-model="user.name"
25536 ng-model-options="{ getterSetter: true }" />
25537 </label>
25538 </form>
25539 <pre>user.name = <span ng-bind="user.name()"></span></pre>
25540 </div>
25541 </file>
25542 <file name="app.js">
25543 angular.module('getterSetterExample', [])
25544 .controller('ExampleController', ['$scope', function($scope) {
25545 var _name = 'Brian';
25546 $scope.user = {
25547 name: function(newName) {
25548 // Note that newName can be undefined for two reasons:
25549 // 1. Because it is called as a getter and thus called with no arguments
25550 // 2. Because the property should actually be set to undefined. This happens e.g. if the
25551 // input is invalid
25552 return arguments.length ? (_name = newName) : _name;
25553 }
25554 };
25555 }]);
25556 </file>
25557 </example>
25558 */
25559 var ngModelOptionsDirective = function() {
25560 return {
25561 restrict: 'A',
25562 controller: ['$scope', '$attrs', function($scope, $attrs) {
25563 var that = this;
25564 this.$options = copy($scope.$eval($attrs.ngModelOptions));
25565 // Allow adding/overriding bound events
25566 if (this.$options.updateOn !== undefined) {
25567 this.$options.updateOnDefault = false;
25568 // extract "default" pseudo-event from list of events that can trigger a model update
25569 this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
25570 that.$options.updateOnDefault = true;
25571 return ' ';
25572 }));
25573 } else {
25574 this.$options.updateOnDefault = true;
25575 }
25576 }]
25577 };
25578 };
25579
25580
25581
25582 // helper methods
25583 function addSetValidityMethod(context) {
25584 var ctrl = context.ctrl,
25585 $element = context.$element,
25586 classCache = {},
25587 set = context.set,
25588 unset = context.unset,
25589 parentForm = context.parentForm,
25590 $animate = context.$animate;
25591
25592 classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
25593
25594 ctrl.$setValidity = setValidity;
25595
25596 function setValidity(validationErrorKey, state, controller) {
25597 if (state === undefined) {
25598 createAndSet('$pending', validationErrorKey, controller);
25599 } else {
25600 unsetAndCleanup('$pending', validationErrorKey, controller);
25601 }
25602 if (!isBoolean(state)) {
25603 unset(ctrl.$error, validationErrorKey, controller);
25604 unset(ctrl.$$success, validationErrorKey, controller);
25605 } else {
25606 if (state) {
25607 unset(ctrl.$error, validationErrorKey, controller);
25608 set(ctrl.$$success, validationErrorKey, controller);
25609 } else {
25610 set(ctrl.$error, validationErrorKey, controller);
25611 unset(ctrl.$$success, validationErrorKey, controller);
25612 }
25613 }
25614 if (ctrl.$pending) {
25615 cachedToggleClass(PENDING_CLASS, true);
25616 ctrl.$valid = ctrl.$invalid = undefined;
25617 toggleValidationCss('', null);
25618 } else {
25619 cachedToggleClass(PENDING_CLASS, false);
25620 ctrl.$valid = isObjectEmpty(ctrl.$error);
25621 ctrl.$invalid = !ctrl.$valid;
25622 toggleValidationCss('', ctrl.$valid);
25623 }
25624
25625 // re-read the state as the set/unset methods could have
25626 // combined state in ctrl.$error[validationError] (used for forms),
25627 // where setting/unsetting only increments/decrements the value,
25628 // and does not replace it.
25629 var combinedState;
25630 if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
25631 combinedState = undefined;
25632 } else if (ctrl.$error[validationErrorKey]) {
25633 combinedState = false;
25634 } else if (ctrl.$$success[validationErrorKey]) {
25635 combinedState = true;
25636 } else {
25637 combinedState = null;
25638 }
25639
25640 toggleValidationCss(validationErrorKey, combinedState);
25641 parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
25642 }
25643
25644 function createAndSet(name, value, controller) {
25645 if (!ctrl[name]) {
25646 ctrl[name] = {};
25647 }
25648 set(ctrl[name], value, controller);
25649 }
25650
25651 function unsetAndCleanup(name, value, controller) {
25652 if (ctrl[name]) {
25653 unset(ctrl[name], value, controller);
25654 }
25655 if (isObjectEmpty(ctrl[name])) {
25656 ctrl[name] = undefined;
25657 }
25658 }
25659
25660 function cachedToggleClass(className, switchValue) {
25661 if (switchValue && !classCache[className]) {
25662 $animate.addClass($element, className);
25663 classCache[className] = true;
25664 } else if (!switchValue && classCache[className]) {
25665 $animate.removeClass($element, className);
25666 classCache[className] = false;
25667 }
25668 }
25669
25670 function toggleValidationCss(validationErrorKey, isValid) {
25671 validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
25672
25673 cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
25674 cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
25675 }
25676 }
25677
25678 function isObjectEmpty(obj) {
25679 if (obj) {
25680 for (var prop in obj) {
25681 if (obj.hasOwnProperty(prop)) {
25682 return false;
25683 }
25684 }
25685 }
25686 return true;
25687 }
25688
25689 /**
25690 * @ngdoc directive
1995025691 * @name ngNonBindable
1995125692 * @restrict AC
1995225693 * @priority 1000
1997925720 */
1998025721 var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
1998125722
25723 /* global jqLiteRemove */
25724
25725 var ngOptionsMinErr = minErr('ngOptions');
25726
25727 /**
25728 * @ngdoc directive
25729 * @name ngOptions
25730 * @restrict A
25731 *
25732 * @description
25733 *
25734 * The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
25735 * elements for the `<select>` element using the array or object obtained by evaluating the
25736 * `ngOptions` comprehension expression.
25737 *
25738 * In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a
25739 * similar result. However, `ngOptions` provides some benefits such as reducing memory and
25740 * increasing speed by not creating a new scope for each repeated instance, as well as providing
25741 * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
25742 * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound
25743 * to a non-string value. This is because an option element can only be bound to string values at
25744 * present.
25745 *
25746 * When an item in the `<select>` menu is selected, the array element or object property
25747 * represented by the selected option will be bound to the model identified by the `ngModel`
25748 * directive.
25749 *
25750 * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
25751 * be nested into the `<select>` element. This element will then represent the `null` or "not selected"
25752 * option. See example below for demonstration.
25753 *
25754 * ## Complex Models (objects or collections)
25755 *
25756 * **Note:** By default, `ngModel` watches the model by reference, not value. This is important when
25757 * binding any input directive to a model that is an object or a collection.
25758 *
25759 * Since this is a common situation for `ngOptions` the directive additionally watches the model using
25760 * `$watchCollection` when the select has the `multiple` attribute or when there is a `track by` clause in
25761 * the options expression. This allows ngOptions to trigger a re-rendering of the options even if the actual
25762 * object/collection has not changed identity but only a property on the object or an item in the collection
25763 * changes.
25764 *
25765 * Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection
25766 * if the model is an array). This means that changing a property deeper inside the object/collection that the
25767 * first level will not trigger a re-rendering.
25768 *
25769 *
25770 * ## `select` **`as`**
25771 *
25772 * Using `select` **`as`** will bind the result of the `select` expression to the model, but
25773 * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
25774 * or property name (for object data sources) of the value within the collection. If a **`track by`** expression
25775 * is used, the result of that expression will be set as the value of the `option` and `select` elements.
25776 *
25777 *
25778 * ### `select` **`as`** and **`track by`**
25779 *
25780 * <div class="alert alert-warning">
25781 * Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
25782 * </div>
25783 *
25784 * Consider the following example:
25785 *
25786 * ```html
25787 * <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
25788 * ```
25789 *
25790 * ```js
25791 * $scope.values = [{
25792 * id: 1,
25793 * label: 'aLabel',
25794 * subItem: { name: 'aSubItem' }
25795 * }, {
25796 * id: 2,
25797 * label: 'bLabel',
25798 * subItem: { name: 'bSubItem' }
25799 * }];
25800 *
25801 * $scope.selected = { name: 'aSubItem' };
25802 * ```
25803 *
25804 * With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
25805 * of the data source (to `item` in this example). To calculate whether an element is selected, we do the
25806 * following:
25807 *
25808 * 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
25809 * 2. Apply **`track by`** to the already selected value in `ngModel`.
25810 * In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
25811 * value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
25812 * a wrong object, the selected element can't be found, `<select>` is always reset to the "not
25813 * selected" option.
25814 *
25815 *
25816 * @param {string} ngModel Assignable angular expression to data-bind to.
25817 * @param {string=} name Property name of the form under which the control is published.
25818 * @param {string=} required The control is considered valid only if value is entered.
25819 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
25820 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
25821 * `required` when you want to data-bind to the `required` attribute.
25822 * @param {comprehension_expression=} ngOptions in one of the following forms:
25823 *
25824 * * for array data sources:
25825 * * `label` **`for`** `value` **`in`** `array`
25826 * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
25827 * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
25828 * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array`
25829 * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
25830 * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
25831 * * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
25832 * (for including a filter with `track by`)
25833 * * for object data sources:
25834 * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
25835 * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
25836 * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
25837 * * `label` **`disable when`** `disable` **`for (`**`key`**`,`** `value`**`) in`** `object`
25838 * * `select` **`as`** `label` **`group by`** `group`
25839 * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
25840 * * `select` **`as`** `label` **`disable when`** `disable`
25841 * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
25842 *
25843 * Where:
25844 *
25845 * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
25846 * * `value`: local variable which will refer to each item in the `array` or each property value
25847 * of `object` during iteration.
25848 * * `key`: local variable which will refer to a property name in `object` during iteration.
25849 * * `label`: The result of this expression will be the label for `<option>` element. The
25850 * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
25851 * * `select`: The result of this expression will be bound to the model of the parent `<select>`
25852 * element. If not specified, `select` expression will default to `value`.
25853 * * `group`: The result of this expression will be used to group options using the `<optgroup>`
25854 * DOM element.
25855 * * `disable`: The result of this expression will be used to disable the rendered `<option>`
25856 * element. Return `true` to disable.
25857 * * `trackexpr`: Used when working with an array of objects. The result of this expression will be
25858 * used to identify the objects in the array. The `trackexpr` will most likely refer to the
25859 * `value` variable (e.g. `value.propertyName`). With this the selection is preserved
25860 * even when the options are recreated (e.g. reloaded from the server).
25861 *
25862 * @example
25863 <example module="selectExample">
25864 <file name="index.html">
25865 <script>
25866 angular.module('selectExample', [])
25867 .controller('ExampleController', ['$scope', function($scope) {
25868 $scope.colors = [
25869 {name:'black', shade:'dark'},
25870 {name:'white', shade:'light', notAnOption: true},
25871 {name:'red', shade:'dark'},
25872 {name:'blue', shade:'dark', notAnOption: true},
25873 {name:'yellow', shade:'light', notAnOption: false}
25874 ];
25875 $scope.myColor = $scope.colors[2]; // red
25876 }]);
25877 </script>
25878 <div ng-controller="ExampleController">
25879 <ul>
25880 <li ng-repeat="color in colors">
25881 <label>Name: <input ng-model="color.name"></label>
25882 <label><input type="checkbox" ng-model="color.notAnOption"> Disabled?</label>
25883 <button ng-click="colors.splice($index, 1)" aria-label="Remove">X</button>
25884 </li>
25885 <li>
25886 <button ng-click="colors.push({})">add</button>
25887 </li>
25888 </ul>
25889 <hr/>
25890 <label>Color (null not allowed):
25891 <select ng-model="myColor" ng-options="color.name for color in colors"></select>
25892 </label><br/>
25893 <label>Color (null allowed):
25894 <span class="nullable">
25895 <select ng-model="myColor" ng-options="color.name for color in colors">
25896 <option value="">-- choose color --</option>
25897 </select>
25898 </span></label><br/>
25899
25900 <label>Color grouped by shade:
25901 <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
25902 </select>
25903 </label><br/>
25904
25905 <label>Color grouped by shade, with some disabled:
25906 <select ng-model="myColor"
25907 ng-options="color.name group by color.shade disable when color.notAnOption for color in colors">
25908 </select>
25909 </label><br/>
25910
25911
25912
25913 Select <button ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</button>.
25914 <br/>
25915 <hr/>
25916 Currently selected: {{ {selected_color:myColor} }}
25917 <div style="border:solid 1px black; height:20px"
25918 ng-style="{'background-color':myColor.name}">
25919 </div>
25920 </div>
25921 </file>
25922 <file name="protractor.js" type="protractor">
25923 it('should check ng-options', function() {
25924 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
25925 element.all(by.model('myColor')).first().click();
25926 element.all(by.css('select[ng-model="myColor"] option')).first().click();
25927 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
25928 element(by.css('.nullable select[ng-model="myColor"]')).click();
25929 element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
25930 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
25931 });
25932 </file>
25933 </example>
25934 */
25935
25936 // jshint maxlen: false
25937 // //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
25938 var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
25939 // 1: value expression (valueFn)
25940 // 2: label expression (displayFn)
25941 // 3: group by expression (groupByFn)
25942 // 4: disable when expression (disableWhenFn)
25943 // 5: array item variable name
25944 // 6: object item key variable name
25945 // 7: object item value variable name
25946 // 8: collection expression
25947 // 9: track by expression
25948 // jshint maxlen: 100
25949
25950
25951 var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
25952
25953 function parseOptionsExpression(optionsExp, selectElement, scope) {
25954
25955 var match = optionsExp.match(NG_OPTIONS_REGEXP);
25956 if (!(match)) {
25957 throw ngOptionsMinErr('iexp',
25958 "Expected expression in form of " +
25959 "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
25960 " but got '{0}'. Element: {1}",
25961 optionsExp, startingTag(selectElement));
25962 }
25963
25964 // Extract the parts from the ngOptions expression
25965
25966 // The variable name for the value of the item in the collection
25967 var valueName = match[5] || match[7];
25968 // The variable name for the key of the item in the collection
25969 var keyName = match[6];
25970
25971 // An expression that generates the viewValue for an option if there is a label expression
25972 var selectAs = / as /.test(match[0]) && match[1];
25973 // An expression that is used to track the id of each object in the options collection
25974 var trackBy = match[9];
25975 // An expression that generates the viewValue for an option if there is no label expression
25976 var valueFn = $parse(match[2] ? match[1] : valueName);
25977 var selectAsFn = selectAs && $parse(selectAs);
25978 var viewValueFn = selectAsFn || valueFn;
25979 var trackByFn = trackBy && $parse(trackBy);
25980
25981 // Get the value by which we are going to track the option
25982 // if we have a trackFn then use that (passing scope and locals)
25983 // otherwise just hash the given viewValue
25984 var getTrackByValueFn = trackBy ?
25985 function(value, locals) { return trackByFn(scope, locals); } :
25986 function getHashOfValue(value) { return hashKey(value); };
25987 var getTrackByValue = function(value, key) {
25988 return getTrackByValueFn(value, getLocals(value, key));
25989 };
25990
25991 var displayFn = $parse(match[2] || match[1]);
25992 var groupByFn = $parse(match[3] || '');
25993 var disableWhenFn = $parse(match[4] || '');
25994 var valuesFn = $parse(match[8]);
25995
25996 var locals = {};
25997 var getLocals = keyName ? function(value, key) {
25998 locals[keyName] = key;
25999 locals[valueName] = value;
26000 return locals;
26001 } : function(value) {
26002 locals[valueName] = value;
26003 return locals;
26004 };
26005
26006
26007 function Option(selectValue, viewValue, label, group, disabled) {
26008 this.selectValue = selectValue;
26009 this.viewValue = viewValue;
26010 this.label = label;
26011 this.group = group;
26012 this.disabled = disabled;
26013 }
26014
26015 function getOptionValuesKeys(optionValues) {
26016 var optionValuesKeys;
26017
26018 if (!keyName && isArrayLike(optionValues)) {
26019 optionValuesKeys = optionValues;
26020 } else {
26021 // if object, extract keys, in enumeration order, unsorted
26022 optionValuesKeys = [];
26023 for (var itemKey in optionValues) {
26024 if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
26025 optionValuesKeys.push(itemKey);
26026 }
26027 }
26028 }
26029 return optionValuesKeys;
26030 }
26031
26032 return {
26033 trackBy: trackBy,
26034 getTrackByValue: getTrackByValue,
26035 getWatchables: $parse(valuesFn, function(optionValues) {
26036 // Create a collection of things that we would like to watch (watchedArray)
26037 // so that they can all be watched using a single $watchCollection
26038 // that only runs the handler once if anything changes
26039 var watchedArray = [];
26040 optionValues = optionValues || [];
26041
26042 var optionValuesKeys = getOptionValuesKeys(optionValues);
26043 var optionValuesLength = optionValuesKeys.length;
26044 for (var index = 0; index < optionValuesLength; index++) {
26045 var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
26046 var value = optionValues[key];
26047
26048 var locals = getLocals(optionValues[key], key);
26049 var selectValue = getTrackByValueFn(optionValues[key], locals);
26050 watchedArray.push(selectValue);
26051
26052 // Only need to watch the displayFn if there is a specific label expression
26053 if (match[2] || match[1]) {
26054 var label = displayFn(scope, locals);
26055 watchedArray.push(label);
26056 }
26057
26058 // Only need to watch the disableWhenFn if there is a specific disable expression
26059 if (match[4]) {
26060 var disableWhen = disableWhenFn(scope, locals);
26061 watchedArray.push(disableWhen);
26062 }
26063 }
26064 return watchedArray;
26065 }),
26066
26067 getOptions: function() {
26068
26069 var optionItems = [];
26070 var selectValueMap = {};
26071
26072 // The option values were already computed in the `getWatchables` fn,
26073 // which must have been called to trigger `getOptions`
26074 var optionValues = valuesFn(scope) || [];
26075 var optionValuesKeys = getOptionValuesKeys(optionValues);
26076 var optionValuesLength = optionValuesKeys.length;
26077
26078 for (var index = 0; index < optionValuesLength; index++) {
26079 var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
26080 var value = optionValues[key];
26081 var locals = getLocals(value, key);
26082 var viewValue = viewValueFn(scope, locals);
26083 var selectValue = getTrackByValueFn(viewValue, locals);
26084 var label = displayFn(scope, locals);
26085 var group = groupByFn(scope, locals);
26086 var disabled = disableWhenFn(scope, locals);
26087 var optionItem = new Option(selectValue, viewValue, label, group, disabled);
26088
26089 optionItems.push(optionItem);
26090 selectValueMap[selectValue] = optionItem;
26091 }
26092
26093 return {
26094 items: optionItems,
26095 selectValueMap: selectValueMap,
26096 getOptionFromViewValue: function(value) {
26097 return selectValueMap[getTrackByValue(value)];
26098 },
26099 getViewValueFromOption: function(option) {
26100 // If the viewValue could be an object that may be mutated by the application,
26101 // we need to make a copy and not return the reference to the value on the option.
26102 return trackBy ? angular.copy(option.viewValue) : option.viewValue;
26103 }
26104 };
26105 }
26106 };
26107 }
26108
26109
26110 // we can't just jqLite('<option>') since jqLite is not smart enough
26111 // to create it in <select> and IE barfs otherwise.
26112 var optionTemplate = document.createElement('option'),
26113 optGroupTemplate = document.createElement('optgroup');
26114
26115 return {
26116 restrict: 'A',
26117 terminal: true,
26118 require: ['select', '?ngModel'],
26119 link: function(scope, selectElement, attr, ctrls) {
26120
26121 // if ngModel is not defined, we don't need to do anything
26122 var ngModelCtrl = ctrls[1];
26123 if (!ngModelCtrl) return;
26124
26125 var selectCtrl = ctrls[0];
26126 var multiple = attr.multiple;
26127
26128 // The emptyOption allows the application developer to provide their own custom "empty"
26129 // option when the viewValue does not match any of the option values.
26130 var emptyOption;
26131 for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
26132 if (children[i].value === '') {
26133 emptyOption = children.eq(i);
26134 break;
26135 }
26136 }
26137
26138 var providedEmptyOption = !!emptyOption;
26139
26140 var unknownOption = jqLite(optionTemplate.cloneNode(false));
26141 unknownOption.val('?');
26142
26143 var options;
26144 var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
26145
26146
26147 var renderEmptyOption = function() {
26148 if (!providedEmptyOption) {
26149 selectElement.prepend(emptyOption);
26150 }
26151 selectElement.val('');
26152 emptyOption.prop('selected', true); // needed for IE
26153 emptyOption.attr('selected', true);
26154 };
26155
26156 var removeEmptyOption = function() {
26157 if (!providedEmptyOption) {
26158 emptyOption.remove();
26159 }
26160 };
26161
26162
26163 var renderUnknownOption = function() {
26164 selectElement.prepend(unknownOption);
26165 selectElement.val('?');
26166 unknownOption.prop('selected', true); // needed for IE
26167 unknownOption.attr('selected', true);
26168 };
26169
26170 var removeUnknownOption = function() {
26171 unknownOption.remove();
26172 };
26173
26174
26175 // Update the controller methods for multiple selectable options
26176 if (!multiple) {
26177
26178 selectCtrl.writeValue = function writeNgOptionsValue(value) {
26179 var option = options.getOptionFromViewValue(value);
26180
26181 if (option && !option.disabled) {
26182 if (selectElement[0].value !== option.selectValue) {
26183 removeUnknownOption();
26184 removeEmptyOption();
26185
26186 selectElement[0].value = option.selectValue;
26187 option.element.selected = true;
26188 option.element.setAttribute('selected', 'selected');
26189 }
26190 } else {
26191 if (value === null || providedEmptyOption) {
26192 removeUnknownOption();
26193 renderEmptyOption();
26194 } else {
26195 removeEmptyOption();
26196 renderUnknownOption();
26197 }
26198 }
26199 };
26200
26201 selectCtrl.readValue = function readNgOptionsValue() {
26202
26203 var selectedOption = options.selectValueMap[selectElement.val()];
26204
26205 if (selectedOption && !selectedOption.disabled) {
26206 removeEmptyOption();
26207 removeUnknownOption();
26208 return options.getViewValueFromOption(selectedOption);
26209 }
26210 return null;
26211 };
26212
26213 // If we are using `track by` then we must watch the tracked value on the model
26214 // since ngModel only watches for object identity change
26215 if (ngOptions.trackBy) {
26216 scope.$watch(
26217 function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
26218 function() { ngModelCtrl.$render(); }
26219 );
26220 }
26221
26222 } else {
26223
26224 ngModelCtrl.$isEmpty = function(value) {
26225 return !value || value.length === 0;
26226 };
26227
26228
26229 selectCtrl.writeValue = function writeNgOptionsMultiple(value) {
26230 options.items.forEach(function(option) {
26231 option.element.selected = false;
26232 });
26233
26234 if (value) {
26235 value.forEach(function(item) {
26236 var option = options.getOptionFromViewValue(item);
26237 if (option && !option.disabled) option.element.selected = true;
26238 });
26239 }
26240 };
26241
26242
26243 selectCtrl.readValue = function readNgOptionsMultiple() {
26244 var selectedValues = selectElement.val() || [],
26245 selections = [];
26246
26247 forEach(selectedValues, function(value) {
26248 var option = options.selectValueMap[value];
26249 if (!option.disabled) selections.push(options.getViewValueFromOption(option));
26250 });
26251
26252 return selections;
26253 };
26254
26255 // If we are using `track by` then we must watch these tracked values on the model
26256 // since ngModel only watches for object identity change
26257 if (ngOptions.trackBy) {
26258
26259 scope.$watchCollection(function() {
26260 if (isArray(ngModelCtrl.$viewValue)) {
26261 return ngModelCtrl.$viewValue.map(function(value) {
26262 return ngOptions.getTrackByValue(value);
26263 });
26264 }
26265 }, function() {
26266 ngModelCtrl.$render();
26267 });
26268
26269 }
26270 }
26271
26272
26273 if (providedEmptyOption) {
26274
26275 // we need to remove it before calling selectElement.empty() because otherwise IE will
26276 // remove the label from the element. wtf?
26277 emptyOption.remove();
26278
26279 // compile the element since there might be bindings in it
26280 $compile(emptyOption)(scope);
26281
26282 // remove the class, which is added automatically because we recompile the element and it
26283 // becomes the compilation root
26284 emptyOption.removeClass('ng-scope');
26285 } else {
26286 emptyOption = jqLite(optionTemplate.cloneNode(false));
26287 }
26288
26289 // We need to do this here to ensure that the options object is defined
26290 // when we first hit it in writeNgOptionsValue
26291 updateOptions();
26292
26293 // We will re-render the option elements if the option values or labels change
26294 scope.$watchCollection(ngOptions.getWatchables, updateOptions);
26295
26296 // ------------------------------------------------------------------ //
26297
26298
26299 function updateOptionElement(option, element) {
26300 option.element = element;
26301 element.disabled = option.disabled;
26302 if (option.value !== element.value) element.value = option.selectValue;
26303 if (option.label !== element.label) {
26304 element.label = option.label;
26305 element.textContent = option.label;
26306 }
26307 }
26308
26309 function addOrReuseElement(parent, current, type, templateElement) {
26310 var element;
26311 // Check whether we can reuse the next element
26312 if (current && lowercase(current.nodeName) === type) {
26313 // The next element is the right type so reuse it
26314 element = current;
26315 } else {
26316 // The next element is not the right type so create a new one
26317 element = templateElement.cloneNode(false);
26318 if (!current) {
26319 // There are no more elements so just append it to the select
26320 parent.appendChild(element);
26321 } else {
26322 // The next element is not a group so insert the new one
26323 parent.insertBefore(element, current);
26324 }
26325 }
26326 return element;
26327 }
26328
26329
26330 function removeExcessElements(current) {
26331 var next;
26332 while (current) {
26333 next = current.nextSibling;
26334 jqLiteRemove(current);
26335 current = next;
26336 }
26337 }
26338
26339
26340 function skipEmptyAndUnknownOptions(current) {
26341 var emptyOption_ = emptyOption && emptyOption[0];
26342 var unknownOption_ = unknownOption && unknownOption[0];
26343
26344 if (emptyOption_ || unknownOption_) {
26345 while (current &&
26346 (current === emptyOption_ ||
26347 current === unknownOption_)) {
26348 current = current.nextSibling;
26349 }
26350 }
26351 return current;
26352 }
26353
26354
26355 function updateOptions() {
26356
26357 var previousValue = options && selectCtrl.readValue();
26358
26359 options = ngOptions.getOptions();
26360
26361 var groupMap = {};
26362 var currentElement = selectElement[0].firstChild;
26363
26364 // Ensure that the empty option is always there if it was explicitly provided
26365 if (providedEmptyOption) {
26366 selectElement.prepend(emptyOption);
26367 }
26368
26369 currentElement = skipEmptyAndUnknownOptions(currentElement);
26370
26371 options.items.forEach(function updateOption(option) {
26372 var group;
26373 var groupElement;
26374 var optionElement;
26375
26376 if (option.group) {
26377
26378 // This option is to live in a group
26379 // See if we have already created this group
26380 group = groupMap[option.group];
26381
26382 if (!group) {
26383
26384 // We have not already created this group
26385 groupElement = addOrReuseElement(selectElement[0],
26386 currentElement,
26387 'optgroup',
26388 optGroupTemplate);
26389 // Move to the next element
26390 currentElement = groupElement.nextSibling;
26391
26392 // Update the label on the group element
26393 groupElement.label = option.group;
26394
26395 // Store it for use later
26396 group = groupMap[option.group] = {
26397 groupElement: groupElement,
26398 currentOptionElement: groupElement.firstChild
26399 };
26400
26401 }
26402
26403 // So now we have a group for this option we add the option to the group
26404 optionElement = addOrReuseElement(group.groupElement,
26405 group.currentOptionElement,
26406 'option',
26407 optionTemplate);
26408 updateOptionElement(option, optionElement);
26409 // Move to the next element
26410 group.currentOptionElement = optionElement.nextSibling;
26411
26412 } else {
26413
26414 // This option is not in a group
26415 optionElement = addOrReuseElement(selectElement[0],
26416 currentElement,
26417 'option',
26418 optionTemplate);
26419 updateOptionElement(option, optionElement);
26420 // Move to the next element
26421 currentElement = optionElement.nextSibling;
26422 }
26423 });
26424
26425
26426 // Now remove all excess options and group
26427 Object.keys(groupMap).forEach(function(key) {
26428 removeExcessElements(groupMap[key].currentOptionElement);
26429 });
26430 removeExcessElements(currentElement);
26431
26432 ngModelCtrl.$render();
26433
26434 // Check to see if the value has changed due to the update to the options
26435 if (!ngModelCtrl.$isEmpty(previousValue)) {
26436 var nextValue = selectCtrl.readValue();
26437 if (ngOptions.trackBy ? !equals(previousValue, nextValue) : previousValue !== nextValue) {
26438 ngModelCtrl.$setViewValue(nextValue);
26439 ngModelCtrl.$render();
26440 }
26441 }
26442
26443 }
26444
26445 }
26446 };
26447 }];
26448
1998226449 /**
1998326450 * @ngdoc directive
1998426451 * @name ngPluralize
2003226499 * into pluralized strings. In the previous example, Angular will replace `{}` with
2003326500 * <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder
2003426501 * for <span ng-non-bindable>{{numberExpression}}</span>.
26502 *
26503 * If no rule is defined for a category, then an empty string is displayed and a warning is generated.
26504 * Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`.
2003526505 *
2003626506 * # Configuring ngPluralize with offset
2003726507 * The `offset` attribute allows further customization of pluralized text, which can result in
2007926549 }]);
2008026550 </script>
2008126551 <div ng-controller="ExampleController">
20082 Person 1:<input type="text" ng-model="person1" value="Igor" /><br/>
20083 Person 2:<input type="text" ng-model="person2" value="Misko" /><br/>
20084 Number of People:<input type="text" ng-model="personCount" value="1" /><br/>
26552 <label>Person 1:<input type="text" ng-model="person1" value="Igor" /></label><br/>
26553 <label>Person 2:<input type="text" ng-model="person2" value="Misko" /></label><br/>
26554 <label>Number of People:<input type="text" ng-model="personCount" value="1" /></label><br/>
2008526555
2008626556 <!--- Example with simple pluralization rules for en locale --->
2008726557 Without Offset:
2015126621 </file>
2015226622 </example>
2015326623 */
20154 var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
20155 var BRACE = /{}/g;
26624 var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, $interpolate, $log) {
26625 var BRACE = /{}/g,
26626 IS_WHEN = /^when(Minus)?(.+)$/;
26627
2015626628 return {
20157 restrict: 'EA',
2015826629 link: function(scope, element, attr) {
2015926630 var numberExp = attr.count,
2016026631 whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs
2016326634 whensExpFns = {},
2016426635 startSymbol = $interpolate.startSymbol(),
2016526636 endSymbol = $interpolate.endSymbol(),
20166 isWhen = /^when(Minus)?(.+)$/;
26637 braceReplacement = startSymbol + numberExp + '-' + offset + endSymbol,
26638 watchRemover = angular.noop,
26639 lastCount;
2016726640
2016826641 forEach(attr, function(expression, attributeName) {
20169 if (isWhen.test(attributeName)) {
20170 whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] =
20171 element.attr(attr.$attr[attributeName]);
26642 var tmpMatch = IS_WHEN.exec(attributeName);
26643 if (tmpMatch) {
26644 var whenKey = (tmpMatch[1] ? '-' : '') + lowercase(tmpMatch[2]);
26645 whens[whenKey] = element.attr(attr.$attr[attributeName]);
2017226646 }
2017326647 });
2017426648 forEach(whens, function(expression, key) {
20175 whensExpFns[key] =
20176 $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' +
20177 offset + endSymbol));
26649 whensExpFns[key] = $interpolate(expression.replace(BRACE, braceReplacement));
26650
2017826651 });
2017926652
20180 scope.$watch(function ngPluralizeWatch() {
20181 var value = parseFloat(scope.$eval(numberExp));
20182
20183 if (!isNaN(value)) {
20184 //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise,
20185 //check it against pluralization rules in $locale service
20186 if (!(value in whens)) value = $locale.pluralCat(value - offset);
20187 return whensExpFns[value](scope, element, true);
20188 } else {
20189 return '';
26653 scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) {
26654 var count = parseFloat(newVal);
26655 var countIsNaN = isNaN(count);
26656
26657 if (!countIsNaN && !(count in whens)) {
26658 // If an explicit number rule such as 1, 2, 3... is defined, just use it.
26659 // Otherwise, check it against pluralization rules in $locale service.
26660 count = $locale.pluralCat(count - offset);
2019026661 }
20191 }, function ngPluralizeWatchAction(newVal) {
20192 element.text(newVal);
26662
26663 // If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
26664 // In JS `NaN !== NaN`, so we have to exlicitly check.
26665 if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
26666 watchRemover();
26667 var whenExpFn = whensExpFns[count];
26668 if (isUndefined(whenExpFn)) {
26669 if (newVal != null) {
26670 $log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp);
26671 }
26672 watchRemover = noop;
26673 updateElementText();
26674 } else {
26675 watchRemover = scope.$watch(whenExpFn, updateElementText);
26676 }
26677 lastCount = count;
26678 }
2019326679 });
26680
26681 function updateElementText(newText) {
26682 element.text(newText || '');
26683 }
2019426684 }
2019526685 };
2019626686 }];
2019826688 /**
2019926689 * @ngdoc directive
2020026690 * @name ngRepeat
26691 * @multiElement
2020126692 *
2020226693 * @description
2020326694 * The `ngRepeat` directive instantiates a template once per item from a collection. Each template
2021726708 *
2021826709 * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
2021926710 * This may be useful when, for instance, nesting ngRepeats.
26711 *
26712 *
26713 * # Iterating over object properties
26714 *
26715 * It is possible to get `ngRepeat` to iterate over the properties of an object using the following
26716 * syntax:
26717 *
26718 * ```js
26719 * <div ng-repeat="(key, value) in myObj"> ... </div>
26720 * ```
26721 *
26722 * You need to be aware that the JavaScript specification does not define the order of keys
26723 * returned for an object. (To mitigate this in Angular 1.3 the `ngRepeat` directive
26724 * used to sort the keys alphabetically.)
26725 *
26726 * Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser
26727 * when running `for key in myObj`. It seems that browsers generally follow the strategy of providing
26728 * keys in the order in which they were defined, although there are exceptions when keys are deleted
26729 * and reinstated. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_issues
26730 *
26731 * If this is not desired, the recommended workaround is to convert your object into an array
26732 * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
26733 * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
26734 * or implement a `$watch` on the object yourself.
26735 *
26736 *
26737 * # Tracking and Duplicates
26738 *
26739 * When the contents of the collection change, `ngRepeat` makes the corresponding changes to the DOM:
26740 *
26741 * * When an item is added, a new instance of the template is added to the DOM.
26742 * * When an item is removed, its template instance is removed from the DOM.
26743 * * When items are reordered, their respective templates are reordered in the DOM.
26744 *
26745 * By default, `ngRepeat` does not allow duplicate items in arrays. This is because when
26746 * there are duplicates, it is not possible to maintain a one-to-one mapping between collection
26747 * items and DOM elements.
26748 *
26749 * If you do need to repeat duplicate items, you can substitute the default tracking behavior
26750 * with your own using the `track by` expression.
26751 *
26752 * For example, you may track items by the index of each item in the collection, using the
26753 * special scope property `$index`:
26754 * ```html
26755 * <div ng-repeat="n in [42, 42, 43, 43] track by $index">
26756 * {{n}}
26757 * </div>
26758 * ```
26759 *
26760 * You may use arbitrary expressions in `track by`, including references to custom functions
26761 * on the scope:
26762 * ```html
26763 * <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
26764 * {{n}}
26765 * </div>
26766 * ```
26767 *
26768 * If you are working with objects that have an identifier property, you can track
26769 * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
26770 * will not have to rebuild the DOM elements for items it has already rendered, even if the
26771 * JavaScript objects in the collection have been substituted for new ones:
26772 * ```html
26773 * <div ng-repeat="model in collection track by model.id">
26774 * {{model.name}}
26775 * </div>
26776 * ```
26777 *
26778 * When no `track by` expression is provided, it is equivalent to tracking by the built-in
26779 * `$id` function, which tracks items by their identity:
26780 * ```html
26781 * <div ng-repeat="obj in collection track by $id(obj)">
26782 * {{obj.prop}}
26783 * </div>
26784 * ```
26785 *
26786 * <div class="alert alert-warning">
26787 * **Note:** `track by` must always be the last expression:
26788 * </div>
26789 * ```
26790 * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id">
26791 * {{model.name}}
26792 * </div>
26793 * ```
2022026794 *
2022126795 * # Special repeat start and end points
2022226796 * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
2028526859 *
2028626860 * For example: `(name, age) in {'adam':10, 'amalie':12}`.
2028726861 *
20288 * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
20289 * which can be used to associate the objects in the collection with the DOM elements. If no tracking function
20290 * is specified the ng-repeat associates elements by identity in the collection. It is an error to have
20291 * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are
20292 * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression,
20293 * before specifying a tracking expression.
26862 * * `variable in expression track by tracking_expression` – You can also provide an optional tracking expression
26863 * which can be used to associate the objects in the collection with the DOM elements. If no tracking expression
26864 * is specified, ng-repeat associates elements by identity. It is an error to have
26865 * more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
26866 * mapped to the same DOM element, which is not possible.)
26867 *
26868 * Note that the tracking expression must come last, after any filters, and the alias expression.
2029426869 *
2029526870 * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements
2029626871 * will be associated by item identity in the array.
2030626881 *
2030726882 * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
2030826883 * to items in conjunction with a tracking expression.
26884 *
26885 * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
26886 * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
26887 * when a filter is active on the repeater, but the filtered result set is empty.
26888 *
26889 * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
26890 * the items have been processed through the filter.
26891 *
26892 * Please note that `as [variable name] is not an operator but rather a part of ngRepeat micro-syntax so it can be used only at the end
26893 * (and not as operator, inside an expression).
26894 *
26895 * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` .
2030926896 *
2031026897 * @example
2031126898 * This example initializes the scope to a list of names and
2032526912 {name:'Samantha', age:60, gender:'girl'}
2032626913 ]">
2032726914 I have {{friends.length}} friends. They are:
20328 <input type="search" ng-model="q" placeholder="filter friends..." />
26915 <input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
2032926916 <ul class="example-animate-container">
20330 <li class="animate-repeat" ng-repeat="friend in friends | filter:q">
26917 <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
2033126918 [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
26919 </li>
26920 <li class="animate-repeat" ng-if="results.length == 0">
26921 <strong>No results found...</strong>
2033226922 </li>
2033326923 </ul>
2033426924 </div>
2039626986 var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
2039726987 var NG_REMOVED = '$$NG_REMOVED';
2039826988 var ngRepeatMinErr = minErr('ngRepeat');
26989
26990 var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
26991 // TODO(perf): generate setters to shave off ~40ms or 1-1.5%
26992 scope[valueIdentifier] = value;
26993 if (keyIdentifier) scope[keyIdentifier] = key;
26994 scope.$index = index;
26995 scope.$first = (index === 0);
26996 scope.$last = (index === (arrayLength - 1));
26997 scope.$middle = !(scope.$first || scope.$last);
26998 // jshint bitwise: false
26999 scope.$odd = !(scope.$even = (index&1) === 0);
27000 // jshint bitwise: true
27001 };
27002
27003 var getBlockStart = function(block) {
27004 return block.clone[0];
27005 };
27006
27007 var getBlockEnd = function(block) {
27008 return block.clone[block.clone.length - 1];
27009 };
27010
27011
2039927012 return {
27013 restrict: 'A',
27014 multiElement: true,
2040027015 transclude: 'element',
2040127016 priority: 1000,
2040227017 terminal: true,
2040327018 $$tlb: true,
20404 link: function($scope, $element, $attr, ctrl, $transclude){
20405 var expression = $attr.ngRepeat;
20406 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),
20407 trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn,
20408 lhs, rhs, valueIdentifier, keyIdentifier,
20409 hashFnLocals = {$id: hashKey};
20410
20411 if (!match) {
20412 throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
27019 compile: function ngRepeatCompile($element, $attr) {
27020 var expression = $attr.ngRepeat;
27021 var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
27022
27023 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*$/);
27024
27025 if (!match) {
27026 throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
2041327027 expression);
20414 }
20415
20416 lhs = match[1];
20417 rhs = match[2];
20418 trackByExp = match[3];
20419
20420 if (trackByExp) {
20421 trackByExpGetter = $parse(trackByExp);
27028 }
27029
27030 var lhs = match[1];
27031 var rhs = match[2];
27032 var aliasAs = match[3];
27033 var trackByExp = match[4];
27034
27035 match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);
27036
27037 if (!match) {
27038 throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
27039 lhs);
27040 }
27041 var valueIdentifier = match[3] || match[1];
27042 var keyIdentifier = match[2];
27043
27044 if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
27045 /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
27046 throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.",
27047 aliasAs);
27048 }
27049
27050 var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
27051 var hashFnLocals = {$id: hashKey};
27052
27053 if (trackByExp) {
27054 trackByExpGetter = $parse(trackByExp);
27055 } else {
27056 trackByIdArrayFn = function(key, value) {
27057 return hashKey(value);
27058 };
27059 trackByIdObjFn = function(key) {
27060 return key;
27061 };
27062 }
27063
27064 return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
27065
27066 if (trackByExpGetter) {
2042227067 trackByIdExpFn = function(key, value, index) {
2042327068 // assign key, value, and $index to the locals so that they can be used in hash functions
2042427069 if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
2042627071 hashFnLocals.$index = index;
2042727072 return trackByExpGetter($scope, hashFnLocals);
2042827073 };
20429 } else {
20430 trackByIdArrayFn = function(key, value) {
20431 return hashKey(value);
20432 };
20433 trackByIdObjFn = function(key) {
20434 return key;
20435 };
2043627074 }
20437
20438 match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
20439 if (!match) {
20440 throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
20441 lhs);
20442 }
20443 valueIdentifier = match[3] || match[1];
20444 keyIdentifier = match[2];
2044527075
2044627076 // Store a list of elements from previous run. This is a hash where key is the item from the
2044727077 // iterator, and the value is objects with following properties.
2044827078 // - scope: bound scope
2044927079 // - element: previous element.
2045027080 // - index: position
20451 var lastBlockMap = {};
27081 //
27082 // We are using no-proto object so that we don't need to guard against inherited props via
27083 // hasOwnProperty.
27084 var lastBlockMap = createMap();
2045227085
2045327086 //watch props
20454 $scope.$watchCollection(rhs, function ngRepeatAction(collection){
27087 $scope.$watchCollection(rhs, function ngRepeatAction(collection) {
2045527088 var index, length,
20456 previousNode = $element[0], // current position of the node
27089 previousNode = $element[0], // node that cloned nodes should be inserted after
27090 // initialized to the comment node anchor
2045727091 nextNode,
2045827092 // Same as lastBlockMap but it has the current state. It will become the
2045927093 // lastBlockMap on the next iteration.
20460 nextBlockMap = {},
20461 arrayLength,
20462 childScope,
27094 nextBlockMap = createMap(),
27095 collectionLength,
2046327096 key, value, // key/value of iteration
2046427097 trackById,
2046527098 trackByIdFn,
2046627099 collectionKeys,
2046727100 block, // last object information {scope, element, id}
20468 nextBlockOrder = [],
27101 nextBlockOrder,
2046927102 elementsToRemove;
2047027103
27104 if (aliasAs) {
27105 $scope[aliasAs] = collection;
27106 }
2047127107
2047227108 if (isArrayLike(collection)) {
2047327109 collectionKeys = collection;
2047427110 trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
2047527111 } else {
2047627112 trackByIdFn = trackByIdExpFn || trackByIdObjFn;
20477 // if object, extract keys, sort them and use to determine order of iteration over obj props
27113 // if object, extract keys, in enumeration order, unsorted
2047827114 collectionKeys = [];
20479 for (key in collection) {
20480 if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
20481 collectionKeys.push(key);
27115 for (var itemKey in collection) {
27116 if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
27117 collectionKeys.push(itemKey);
2048227118 }
2048327119 }
20484 collectionKeys.sort();
2048527120 }
2048627121
20487 arrayLength = collectionKeys.length;
27122 collectionLength = collectionKeys.length;
27123 nextBlockOrder = new Array(collectionLength);
2048827124
2048927125 // locate existing items
20490 length = nextBlockOrder.length = collectionKeys.length;
20491 for(index = 0; index < length; index++) {
20492 key = (collection === collectionKeys) ? index : collectionKeys[index];
20493 value = collection[key];
20494 trackById = trackByIdFn(key, value, index);
20495 assertNotHasOwnProperty(trackById, '`track by` id');
20496 if(lastBlockMap.hasOwnProperty(trackById)) {
20497 block = lastBlockMap[trackById];
20498 delete lastBlockMap[trackById];
20499 nextBlockMap[trackById] = block;
20500 nextBlockOrder[index] = block;
20501 } else if (nextBlockMap.hasOwnProperty(trackById)) {
20502 // restore lastBlockMap
20503 forEach(nextBlockOrder, function(block) {
20504 if (block && block.scope) lastBlockMap[block.id] = block;
20505 });
20506 // This is a duplicate and we need to throw an error
20507 throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}",
20508 expression, trackById);
20509 } else {
20510 // new never before seen block
20511 nextBlockOrder[index] = { id: trackById };
20512 nextBlockMap[trackById] = false;
20513 }
20514 }
20515
20516 // remove existing items
20517 for (key in lastBlockMap) {
20518 // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn
20519 if (lastBlockMap.hasOwnProperty(key)) {
20520 block = lastBlockMap[key];
20521 elementsToRemove = getBlockElements(block.clone);
20522 $animate.leave(elementsToRemove);
20523 forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; });
20524 block.scope.$destroy();
27126 for (index = 0; index < collectionLength; index++) {
27127 key = (collection === collectionKeys) ? index : collectionKeys[index];
27128 value = collection[key];
27129 trackById = trackByIdFn(key, value, index);
27130 if (lastBlockMap[trackById]) {
27131 // found previously seen block
27132 block = lastBlockMap[trackById];
27133 delete lastBlockMap[trackById];
27134 nextBlockMap[trackById] = block;
27135 nextBlockOrder[index] = block;
27136 } else if (nextBlockMap[trackById]) {
27137 // if collision detected. restore lastBlockMap and throw an error
27138 forEach(nextBlockOrder, function(block) {
27139 if (block && block.scope) lastBlockMap[block.id] = block;
27140 });
27141 throw ngRepeatMinErr('dupes',
27142 "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
27143 expression, trackById, value);
27144 } else {
27145 // new never before seen block
27146 nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
27147 nextBlockMap[trackById] = true;
2052527148 }
2052627149 }
2052727150
27151 // remove leftover items
27152 for (var blockKey in lastBlockMap) {
27153 block = lastBlockMap[blockKey];
27154 elementsToRemove = getBlockNodes(block.clone);
27155 $animate.leave(elementsToRemove);
27156 if (elementsToRemove[0].parentNode) {
27157 // if the element was not removed yet because of pending animation, mark it as deleted
27158 // so that we can ignore it later
27159 for (index = 0, length = elementsToRemove.length; index < length; index++) {
27160 elementsToRemove[index][NG_REMOVED] = true;
27161 }
27162 }
27163 block.scope.$destroy();
27164 }
27165
2052827166 // we are not using forEach for perf reasons (trying to avoid #call)
20529 for (index = 0, length = collectionKeys.length; index < length; index++) {
27167 for (index = 0; index < collectionLength; index++) {
2053027168 key = (collection === collectionKeys) ? index : collectionKeys[index];
2053127169 value = collection[key];
2053227170 block = nextBlockOrder[index];
20533 if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]);
2053427171
2053527172 if (block.scope) {
2053627173 // if we have already seen this object, then we need to reuse the
2053727174 // associated scope/element
20538 childScope = block.scope;
2053927175
2054027176 nextNode = previousNode;
27177
27178 // skip nodes that are already pending removal via leave animation
2054127179 do {
2054227180 nextNode = nextNode.nextSibling;
20543 } while(nextNode && nextNode[NG_REMOVED]);
27181 } while (nextNode && nextNode[NG_REMOVED]);
2054427182
2054527183 if (getBlockStart(block) != nextNode) {
2054627184 // existing item which got moved
20547 $animate.move(getBlockElements(block.clone), null, jqLite(previousNode));
27185 $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode));
2054827186 }
2054927187 previousNode = getBlockEnd(block);
27188 updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
2055027189 } else {
2055127190 // new item which we don't know about
20552 childScope = $scope.$new();
20553 }
20554
20555 childScope[valueIdentifier] = value;
20556 if (keyIdentifier) childScope[keyIdentifier] = key;
20557 childScope.$index = index;
20558 childScope.$first = (index === 0);
20559 childScope.$last = (index === (arrayLength - 1));
20560 childScope.$middle = !(childScope.$first || childScope.$last);
20561 // jshint bitwise: false
20562 childScope.$odd = !(childScope.$even = (index&1) === 0);
20563 // jshint bitwise: true
20564
20565 if (!block.scope) {
20566 $transclude(childScope, function(clone) {
20567 clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' ');
27191 $transclude(function ngRepeatTransclude(clone, scope) {
27192 block.scope = scope;
27193 // http://jsperf.com/clone-vs-createcomment
27194 var endNode = ngRepeatEndComment.cloneNode(false);
27195 clone[clone.length++] = endNode;
27196
27197 // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper?
2056827198 $animate.enter(clone, null, jqLite(previousNode));
20569 previousNode = clone;
20570 block.scope = childScope;
27199 previousNode = endNode;
2057127200 // Note: We only need the first/last node of the cloned nodes.
2057227201 // However, we need to keep the reference to the jqlite wrapper as it might be changed later
2057327202 // by a directive with templateUrl when its template arrives.
2057427203 block.clone = clone;
2057527204 nextBlockMap[block.id] = block;
27205 updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
2057627206 });
2057727207 }
2057827208 }
2057927209 lastBlockMap = nextBlockMap;
2058027210 });
27211 };
2058127212 }
2058227213 };
20583
20584 function getBlockStart(block) {
20585 return block.clone[0];
20586 }
20587
20588 function getBlockEnd(block) {
20589 return block.clone[block.clone.length - 1];
20590 }
2059127214 }];
2059227215
27216 var NG_HIDE_CLASS = 'ng-hide';
27217 var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
2059327218 /**
2059427219 * @ngdoc directive
2059527220 * @name ngShow
27221 * @multiElement
2059627222 *
2059727223 * @description
2059827224 * The `ngShow` directive shows or hides the given HTML element based on the expression
20599 * provided to the ngShow attribute. The element is shown or hidden by removing or adding
20600 * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
27225 * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding
27226 * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
2060127227 * in AngularJS and sets the display style to none (using an !important flag).
2060227228 * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
2060327229 *
2060927235 * <div ng-show="myValue" class="ng-hide"></div>
2061027236 * ```
2061127237 *
20612 * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute
20613 * on the element causing it to become hidden. When true, the ng-hide CSS class is removed
27238 * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class
27239 * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed
2061427240 * from the element causing the element not to appear hidden.
2061527241 *
20616 * <div class="alert alert-warning">
20617 * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):<br />
20618 * "f" / "0" / "false" / "no" / "n" / "[]"
20619 * </div>
20620 *
2062127242 * ## Why is !important used?
2062227243 *
20623 * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector
27244 * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
2062427245 * can be easily overridden by heavier selectors. For example, something as simple
2062527246 * as changing the display style on a HTML list item would make hidden elements appear visible.
2062627247 * This also becomes a bigger issue when dealing with CSS frameworks.
2062927250 * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
2063027251 * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
2063127252 *
20632 * ### Overriding .ng-hide
20633 *
20634 * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change
27253 * ### Overriding `.ng-hide`
27254 *
27255 * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
2063527256 * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
20636 * class in CSS:
27257 * class CSS. Note that the selector that needs to be used is actually `.ng-hide:not(.ng-hide-animate)` to cope
27258 * with extra animation classes that can be added.
2063727259 *
2063827260 * ```css
20639 * .ng-hide {
20640 * //this is just another form of hiding an element
20641 * display:block!important;
20642 * position:absolute;
20643 * top:-9999px;
20644 * left:-9999px;
27261 * .ng-hide:not(.ng-hide-animate) {
27262 * /&#42; this is just another form of hiding an element &#42;/
27263 * display: block!important;
27264 * position: absolute;
27265 * top: -9999px;
27266 * left: -9999px;
2064527267 * }
2064627268 * ```
2064727269 *
2064827270 * By default you don't need to override in CSS anything and the animations will work around the display style.
2064927271 *
20650 * ## A note about animations with ngShow
27272 * ## A note about animations with `ngShow`
2065127273 *
2065227274 * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
2065327275 * is true and false. This system works like the animation system present with ngClass except that
2065927281 * //a working example can be found at the bottom of this page
2066027282 * //
2066127283 * .my-element.ng-hide-add, .my-element.ng-hide-remove {
20662 * transition:0.5s linear all;
27284 * /&#42; this is required as of 1.3x to properly
27285 * apply all styling in a show/hide animation &#42;/
27286 * transition: 0s linear all;
27287 * }
27288 *
27289 * .my-element.ng-hide-add-active,
27290 * .my-element.ng-hide-remove-active {
27291 * /&#42; the transition is defined in the active class &#42;/
27292 * transition: 1s linear all;
2066327293 * }
2066427294 *
2066527295 * .my-element.ng-hide-add { ... }
2066827298 * .my-element.ng-hide-remove.ng-hide-remove-active { ... }
2066927299 * ```
2067027300 *
20671 * 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
27301 * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
2067227302 * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
2067327303 *
2067427304 * @animations
20675 * addClass: .ng-hide - happens after the ngShow expression evaluates to a truthy value and the just before contents are set to visible
20676 * removeClass: .ng-hide - happens after the ngShow expression evaluates to a non truthy value and just before the contents are set to hidden
27305 * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible
27306 * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden
2067727307 *
2067827308 * @element ANY
2067927309 * @param {expression} ngShow If the {@link guide/expression expression} is truthy
2068227312 * @example
2068327313 <example module="ngAnimate" deps="angular-animate.js" animations="true">
2068427314 <file name="index.html">
20685 Click me: <input type="checkbox" ng-model="checked"><br/>
27315 Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/>
2068627316 <div>
2068727317 Show:
2068827318 <div class="check-element animate-show" ng-show="checked">
2069727327 </div>
2069827328 </file>
2069927329 <file name="glyphicons.css">
20700 @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css);
27330 @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
2070127331 </file>
2070227332 <file name="animations.css">
2070327333 .animate-show {
20704 -webkit-transition:all linear 0.5s;
20705 transition:all linear 0.5s;
20706 line-height:20px;
20707 opacity:1;
20708 padding:10px;
20709 border:1px solid black;
20710 background:white;
27334 line-height: 20px;
27335 opacity: 1;
27336 padding: 10px;
27337 border: 1px solid black;
27338 background: white;
27339 }
27340
27341 .animate-show.ng-hide-add.ng-hide-add-active,
27342 .animate-show.ng-hide-remove.ng-hide-remove-active {
27343 -webkit-transition: all linear 0.5s;
27344 transition: all linear 0.5s;
2071127345 }
2071227346
2071327347 .animate-show.ng-hide {
20714 line-height:0;
20715 opacity:0;
20716 padding:0 10px;
27348 line-height: 0;
27349 opacity: 0;
27350 padding: 0 10px;
2071727351 }
2071827352
2071927353 .check-element {
20720 padding:10px;
20721 border:1px solid black;
20722 background:white;
27354 padding: 10px;
27355 border: 1px solid black;
27356 background: white;
2072327357 }
2072427358 </file>
2072527359 <file name="protractor.js" type="protractor">
2073927373 </example>
2074027374 */
2074127375 var ngShowDirective = ['$animate', function($animate) {
20742 return function(scope, element, attr) {
20743 scope.$watch(attr.ngShow, function ngShowWatchAction(value){
20744 $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
20745 });
27376 return {
27377 restrict: 'A',
27378 multiElement: true,
27379 link: function(scope, element, attr) {
27380 scope.$watch(attr.ngShow, function ngShowWatchAction(value) {
27381 // we're adding a temporary, animation-specific class for ng-hide since this way
27382 // we can control when the element is actually displayed on screen without having
27383 // to have a global/greedy CSS selector that breaks when other animations are run.
27384 // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845
27385 $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, {
27386 tempClasses: NG_HIDE_IN_PROGRESS_CLASS
27387 });
27388 });
27389 }
2074627390 };
2074727391 }];
2074827392
2075027394 /**
2075127395 * @ngdoc directive
2075227396 * @name ngHide
27397 * @multiElement
2075327398 *
2075427399 * @description
2075527400 * The `ngHide` directive shows or hides the given HTML element based on the expression
20756 * provided to the ngHide attribute. The element is shown or hidden by removing or adding
27401 * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding
2075727402 * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
2075827403 * in AngularJS and sets the display style to none (using an !important flag).
2075927404 * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
2076627411 * <div ng-hide="myValue"></div>
2076727412 * ```
2076827413 *
20769 * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute
20770 * on the element causing it to become hidden. When false, the ng-hide CSS class is removed
27414 * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class
27415 * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed
2077127416 * from the element causing the element not to appear hidden.
2077227417 *
20773 * <div class="alert alert-warning">
20774 * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):<br />
20775 * "f" / "0" / "false" / "no" / "n" / "[]"
20776 * </div>
20777 *
2077827418 * ## Why is !important used?
2077927419 *
20780 * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector
27420 * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
2078127421 * can be easily overridden by heavier selectors. For example, something as simple
2078227422 * as changing the display style on a HTML list item would make hidden elements appear visible.
2078327423 * This also becomes a bigger issue when dealing with CSS frameworks.
2078627426 * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
2078727427 * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
2078827428 *
20789 * ### Overriding .ng-hide
20790 *
20791 * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change
27429 * ### Overriding `.ng-hide`
27430 *
27431 * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
2079227432 * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
2079327433 * class in CSS:
2079427434 *
2079527435 * ```css
2079627436 * .ng-hide {
20797 * //this is just another form of hiding an element
20798 * display:block!important;
20799 * position:absolute;
20800 * top:-9999px;
20801 * left:-9999px;
27437 * /&#42; this is just another form of hiding an element &#42;/
27438 * display: block!important;
27439 * position: absolute;
27440 * top: -9999px;
27441 * left: -9999px;
2080227442 * }
2080327443 * ```
2080427444 *
2080527445 * By default you don't need to override in CSS anything and the animations will work around the display style.
2080627446 *
20807 * ## A note about animations with ngHide
27447 * ## A note about animations with `ngHide`
2080827448 *
2080927449 * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
2081027450 * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide`
2081527455 * //a working example can be found at the bottom of this page
2081627456 * //
2081727457 * .my-element.ng-hide-add, .my-element.ng-hide-remove {
20818 * transition:0.5s linear all;
27458 * transition: 0.5s linear all;
2081927459 * }
2082027460 *
2082127461 * .my-element.ng-hide-add { ... }
2082427464 * .my-element.ng-hide-remove.ng-hide-remove-active { ... }
2082527465 * ```
2082627466 *
20827 * 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
27467 * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
2082827468 * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
2082927469 *
2083027470 * @animations
20831 * removeClass: .ng-hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden
20832 * addClass: .ng-hide - happens after the ngHide expression evaluates to a non truthy value and just before the contents are set to visible
27471 * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden
27472 * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible
2083327473 *
2083427474 * @element ANY
2083527475 * @param {expression} ngHide If the {@link guide/expression expression} is truthy then
2083827478 * @example
2083927479 <example module="ngAnimate" deps="angular-animate.js" animations="true">
2084027480 <file name="index.html">
20841 Click me: <input type="checkbox" ng-model="checked"><br/>
27481 Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/>
2084227482 <div>
2084327483 Show:
2084427484 <div class="check-element animate-hide" ng-show="checked">
2085327493 </div>
2085427494 </file>
2085527495 <file name="glyphicons.css">
20856 @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css);
27496 @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
2085727497 </file>
2085827498 <file name="animations.css">
2085927499 .animate-hide {
20860 -webkit-transition:all linear 0.5s;
20861 transition:all linear 0.5s;
20862 line-height:20px;
20863 opacity:1;
20864 padding:10px;
20865 border:1px solid black;
20866 background:white;
27500 -webkit-transition: all linear 0.5s;
27501 transition: all linear 0.5s;
27502 line-height: 20px;
27503 opacity: 1;
27504 padding: 10px;
27505 border: 1px solid black;
27506 background: white;
2086727507 }
2086827508
2086927509 .animate-hide.ng-hide {
20870 line-height:0;
20871 opacity:0;
20872 padding:0 10px;
27510 line-height: 0;
27511 opacity: 0;
27512 padding: 0 10px;
2087327513 }
2087427514
2087527515 .check-element {
20876 padding:10px;
20877 border:1px solid black;
20878 background:white;
27516 padding: 10px;
27517 border: 1px solid black;
27518 background: white;
2087927519 }
2088027520 </file>
2088127521 <file name="protractor.js" type="protractor">
2089527535 </example>
2089627536 */
2089727537 var ngHideDirective = ['$animate', function($animate) {
20898 return function(scope, element, attr) {
20899 scope.$watch(attr.ngHide, function ngHideWatchAction(value){
20900 $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide');
20901 });
27538 return {
27539 restrict: 'A',
27540 multiElement: true,
27541 link: function(scope, element, attr) {
27542 scope.$watch(attr.ngHide, function ngHideWatchAction(value) {
27543 // The comment inside of the ngShowDirective explains why we add and
27544 // remove a temporary class for the show/hide animation
27545 $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, {
27546 tempClasses: NG_HIDE_IN_PROGRESS_CLASS
27547 });
27548 });
27549 }
2090227550 };
2090327551 }];
2090427552
2099927647 *
2100027648 *
2100127649 * @scope
21002 * @priority 800
21003 * @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>.
27650 * @priority 1200
27651 * @param {*} ngSwitch|on expression to match against <code>ng-switch-when</code>.
2100427652 * On child elements add:
2100527653 *
2100627654 * * `ngSwitchWhen`: the case statement to match against. If match then this
2101727665 <div ng-controller="ExampleController">
2101827666 <select ng-model="selection" ng-options="item for item in items">
2101927667 </select>
21020 <tt>selection={{selection}}</tt>
27668 <code>selection={{selection}}</code>
2102127669 <hr/>
2102227670 <div class="animate-switch-container"
2102327671 ng-switch on="selection">
2108727735 */
2108827736 var ngSwitchDirective = ['$animate', function($animate) {
2108927737 return {
21090 restrict: 'EA',
2109127738 require: 'ngSwitch',
2109227739
2109327740 // asks for $scope to fool the BC controller module
2109827745 var watchExpr = attr.ngSwitch || attr.on,
2109927746 selectedTranscludes = [],
2110027747 selectedElements = [],
21101 previousElements = [],
27748 previousLeaveAnimations = [],
2110227749 selectedScopes = [];
27750
27751 var spliceFactory = function(array, index) {
27752 return function() { array.splice(index, 1); };
27753 };
2110327754
2110427755 scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
2110527756 var i, ii;
21106 for (i = 0, ii = previousElements.length; i < ii; ++i) {
21107 previousElements[i].remove();
27757 for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
27758 $animate.cancel(previousLeaveAnimations[i]);
2110827759 }
21109 previousElements.length = 0;
27760 previousLeaveAnimations.length = 0;
2111027761
2111127762 for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
21112 var selected = selectedElements[i];
27763 var selected = getBlockNodes(selectedElements[i].clone);
2111327764 selectedScopes[i].$destroy();
21114 previousElements[i] = selected;
21115 $animate.leave(selected, function() {
21116 previousElements.splice(i, 1);
21117 });
27765 var promise = previousLeaveAnimations[i] = $animate.leave(selected);
27766 promise.then(spliceFactory(previousLeaveAnimations, i));
2111827767 }
2111927768
2112027769 selectedElements.length = 0;
2112127770 selectedScopes.length = 0;
2112227771
2112327772 if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) {
21124 scope.$eval(attr.change);
2112527773 forEach(selectedTranscludes, function(selectedTransclude) {
21126 var selectedScope = scope.$new();
21127 selectedScopes.push(selectedScope);
21128 selectedTransclude.transclude(selectedScope, function(caseElement) {
27774 selectedTransclude.transclude(function(caseElement, selectedScope) {
27775 selectedScopes.push(selectedScope);
2112927776 var anchor = selectedTransclude.element;
21130
21131 selectedElements.push(caseElement);
27777 caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: ');
27778 var block = { clone: caseElement };
27779
27780 selectedElements.push(block);
2113227781 $animate.enter(caseElement, anchor.parent(), anchor);
2113327782 });
2113427783 });
2114027789
2114127790 var ngSwitchWhenDirective = ngDirective({
2114227791 transclude: 'element',
21143 priority: 800,
27792 priority: 1200,
2114427793 require: '^ngSwitch',
27794 multiElement: true,
2114527795 link: function(scope, element, attrs, ctrl, $transclude) {
2114627796 ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []);
2114727797 ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element });
2115027800
2115127801 var ngSwitchDefaultDirective = ngDirective({
2115227802 transclude: 'element',
21153 priority: 800,
27803 priority: 1200,
2115427804 require: '^ngSwitch',
27805 multiElement: true,
2115527806 link: function(scope, element, attr, ctrl, $transclude) {
2115627807 ctrl.cases['?'] = (ctrl.cases['?'] || []);
2115727808 ctrl.cases['?'].push({ transclude: $transclude, element: element });
2116127812 /**
2116227813 * @ngdoc directive
2116327814 * @name ngTransclude
21164 * @restrict AC
27815 * @restrict EAC
2116527816 *
2116627817 * @description
2116727818 * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
2118227833 scope: { title:'@' },
2118327834 template: '<div style="border: 1px solid black;">' +
2118427835 '<div style="background-color: gray">{{title}}</div>' +
21185 '<div ng-transclude></div>' +
27836 '<ng-transclude></ng-transclude>' +
2118627837 '</div>'
2118727838 };
2118827839 })
2119227843 }]);
2119327844 </script>
2119427845 <div ng-controller="ExampleController">
21195 <input ng-model="title"><br>
21196 <textarea ng-model="text"></textarea> <br/>
27846 <input ng-model="title" aria-label="title"> <br/>
27847 <textarea ng-model="text" aria-label="text"></textarea> <br/>
2119727848 <pane title="{{title}}">{{text}}</pane>
2119827849 </div>
2119927850 </file>
2121327864 *
2121427865 */
2121527866 var ngTranscludeDirective = ngDirective({
27867 restrict: 'EAC',
2121627868 link: function($scope, $element, $attrs, controller, $transclude) {
2121727869 if (!$transclude) {
2121827870 throw minErr('ngTransclude')('orphan',
2126927921 compile: function(element, attr) {
2127027922 if (attr.type == 'text/ng-template') {
2127127923 var templateUrl = attr.id,
21272 // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent
2127327924 text = element[0].text;
2127427925
2127527926 $templateCache.put(templateUrl, text);
2127827929 };
2127927930 }];
2128027931
21281 var ngOptionsMinErr = minErr('ngOptions');
27932 var noopNgModelController = { $setViewValue: noop, $render: noop };
27933
27934 /**
27935 * @ngdoc type
27936 * @name select.SelectController
27937 * @description
27938 * The controller for the `<select>` directive. This provides support for reading
27939 * and writing the selected value(s) of the control and also coordinates dynamically
27940 * added `<option>` elements, perhaps by an `ngRepeat` directive.
27941 */
27942 var SelectController =
27943 ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
27944
27945 var self = this,
27946 optionsMap = new HashMap();
27947
27948 // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
27949 self.ngModelCtrl = noopNgModelController;
27950
27951 // The "unknown" option is one that is prepended to the list if the viewValue
27952 // does not match any of the options. When it is rendered the value of the unknown
27953 // option is '? XXX ?' where XXX is the hashKey of the value that is not known.
27954 //
27955 // We can't just jqLite('<option>') since jqLite is not smart enough
27956 // to create it in <select> and IE barfs otherwise.
27957 self.unknownOption = jqLite(document.createElement('option'));
27958 self.renderUnknownOption = function(val) {
27959 var unknownVal = '? ' + hashKey(val) + ' ?';
27960 self.unknownOption.val(unknownVal);
27961 $element.prepend(self.unknownOption);
27962 $element.val(unknownVal);
27963 };
27964
27965 $scope.$on('$destroy', function() {
27966 // disable unknown option so that we don't do work when the whole select is being destroyed
27967 self.renderUnknownOption = noop;
27968 });
27969
27970 self.removeUnknownOption = function() {
27971 if (self.unknownOption.parent()) self.unknownOption.remove();
27972 };
27973
27974
27975 // Read the value of the select control, the implementation of this changes depending
27976 // upon whether the select can have multiple values and whether ngOptions is at work.
27977 self.readValue = function readSingleValue() {
27978 self.removeUnknownOption();
27979 return $element.val();
27980 };
27981
27982
27983 // Write the value to the select control, the implementation of this changes depending
27984 // upon whether the select can have multiple values and whether ngOptions is at work.
27985 self.writeValue = function writeSingleValue(value) {
27986 if (self.hasOption(value)) {
27987 self.removeUnknownOption();
27988 $element.val(value);
27989 if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
27990 } else {
27991 if (value == null && self.emptyOption) {
27992 self.removeUnknownOption();
27993 $element.val('');
27994 } else {
27995 self.renderUnknownOption(value);
27996 }
27997 }
27998 };
27999
28000
28001 // Tell the select control that an option, with the given value, has been added
28002 self.addOption = function(value, element) {
28003 assertNotHasOwnProperty(value, '"option value"');
28004 if (value === '') {
28005 self.emptyOption = element;
28006 }
28007 var count = optionsMap.get(value) || 0;
28008 optionsMap.put(value, count + 1);
28009 };
28010
28011 // Tell the select control that an option, with the given value, has been removed
28012 self.removeOption = function(value) {
28013 var count = optionsMap.get(value);
28014 if (count) {
28015 if (count === 1) {
28016 optionsMap.remove(value);
28017 if (value === '') {
28018 self.emptyOption = undefined;
28019 }
28020 } else {
28021 optionsMap.put(value, count - 1);
28022 }
28023 }
28024 };
28025
28026 // Check whether the select control has an option matching the given value
28027 self.hasOption = function(value) {
28028 return !!optionsMap.get(value);
28029 };
28030 }];
28031
2128228032 /**
2128328033 * @ngdoc directive
2128428034 * @name select
2128728037 * @description
2128828038 * HTML `SELECT` element with angular data-binding.
2128928039 *
21290 * # `ngOptions`
21291 *
21292 * The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
21293 * elements for the `<select>` element using the array or object obtained by evaluating the
21294 * `ngOptions` comprehension_expression.
28040 * In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
28041 * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits such as reducing
28042 * memory and increasing speed by not creating a new scope for each repeated instance, as well as providing
28043 * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
28044 * comprehension expression.
2129528045 *
2129628046 * When an item in the `<select>` menu is selected, the array element or object property
2129728047 * represented by the selected option will be bound to the model identified by the `ngModel`
2129828048 * directive.
2129928049 *
21300 * <div class="alert alert-warning">
21301 * **Note:** `ngModel` compares by reference, not value. This is important when binding to an
21302 * array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/).
21303 * </div>
28050 * If the viewValue contains a value that doesn't match any of the options then the control
28051 * will automatically add an "unknown" option, which it then removes when this is resolved.
2130428052 *
2130528053 * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
2130628054 * be nested into the `<select>` element. This element will then represent the `null` or "not selected"
2130728055 * option. See example below for demonstration.
2130828056 *
21309 * <div class="alert alert-warning">
21310 * **Note:** `ngOptions` provides an iterator facility for the `<option>` element which should be used instead
21311 * of {@link ng.directive:ngRepeat ngRepeat} when you want the
21312 * `select` model to be bound to a non-string value. This is because an option element can only
21313 * be bound to string values at present.
28057 * <div class="alert alert-info">
28058 * The value of a `select` directive used without `ngOptions` is always a string.
28059 * When the model needs to be bound to a non-string value, you must either explictly convert it
28060 * using a directive (see example below) or use `ngOptions` to specify the set of options.
28061 * This is because an option element can only be bound to string values at present.
2131428062 * </div>
2131528063 *
21316 * @param {string} ngModel Assignable angular expression to data-bind to.
21317 * @param {string=} name Property name of the form under which the control is published.
21318 * @param {string=} required The control is considered valid only if value is entered.
21319 * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
21320 * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
21321 * `required` when you want to data-bind to the `required` attribute.
21322 * @param {comprehension_expression=} ngOptions in one of the following forms:
21323 *
21324 * * for array data sources:
21325 * * `label` **`for`** `value` **`in`** `array`
21326 * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
21327 * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
21328 * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
21329 * * for object data sources:
21330 * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
21331 * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
21332 * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
21333 * * `select` **`as`** `label` **`group by`** `group`
21334 * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
21335 *
21336 * Where:
21337 *
21338 * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
21339 * * `value`: local variable which will refer to each item in the `array` or each property value
21340 * of `object` during iteration.
21341 * * `key`: local variable which will refer to a property name in `object` during iteration.
21342 * * `label`: The result of this expression will be the label for `<option>` element. The
21343 * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
21344 * * `select`: The result of this expression will be bound to the model of the parent `<select>`
21345 * element. If not specified, `select` expression will default to `value`.
21346 * * `group`: The result of this expression will be used to group options using the `<optgroup>`
21347 * DOM element.
21348 * * `trackexpr`: Used when working with an array of objects. The result of this expression will be
21349 * used to identify the objects in the array. The `trackexpr` will most likely refer to the
21350 * `value` variable (e.g. `value.propertyName`).
21351 *
21352 * @example
21353 <example module="selectExample">
21354 <file name="index.html">
21355 <script>
21356 angular.module('selectExample', [])
21357 .controller('ExampleController', ['$scope', function($scope) {
21358 $scope.colors = [
21359 {name:'black', shade:'dark'},
21360 {name:'white', shade:'light'},
21361 {name:'red', shade:'dark'},
21362 {name:'blue', shade:'dark'},
21363 {name:'yellow', shade:'light'}
21364 ];
21365 $scope.myColor = $scope.colors[2]; // red
21366 }]);
21367 </script>
21368 <div ng-controller="ExampleController">
21369 <ul>
21370 <li ng-repeat="color in colors">
21371 Name: <input ng-model="color.name">
21372 [<a href ng-click="colors.splice($index, 1)">X</a>]
21373 </li>
21374 <li>
21375 [<a href ng-click="colors.push({})">add</a>]
21376 </li>
21377 </ul>
21378 <hr/>
21379 Color (null not allowed):
21380 <select ng-model="myColor" ng-options="color.name for color in colors"></select><br>
21381
21382 Color (null allowed):
21383 <span class="nullable">
21384 <select ng-model="myColor" ng-options="color.name for color in colors">
21385 <option value="">-- choose color --</option>
21386 </select>
21387 </span><br/>
21388
21389 Color grouped by shade:
21390 <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
21391 </select><br/>
21392
21393
21394 Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
21395 <hr/>
21396 Currently selected: {{ {selected_color:myColor} }}
21397 <div style="border:solid 1px black; height:20px"
21398 ng-style="{'background-color':myColor.name}">
21399 </div>
21400 </div>
21401 </file>
21402 <file name="protractor.js" type="protractor">
21403 it('should check ng-options', function() {
21404 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
21405 element.all(by.model('myColor')).first().click();
21406 element.all(by.css('select[ng-model="myColor"] option')).first().click();
21407 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
21408 element(by.css('.nullable select[ng-model="myColor"]')).click();
21409 element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
21410 expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
21411 });
21412 </file>
21413 </example>
28064 * ### Example (binding `select` to a non-string value)
28065 *
28066 * <example name="select-with-non-string-options" module="nonStringSelect">
28067 * <file name="index.html">
28068 * <select ng-model="model.id" convert-to-number>
28069 * <option value="0">Zero</option>
28070 * <option value="1">One</option>
28071 * <option value="2">Two</option>
28072 * </select>
28073 * {{ model }}
28074 * </file>
28075 * <file name="app.js">
28076 * angular.module('nonStringSelect', [])
28077 * .run(function($rootScope) {
28078 * $rootScope.model = { id: 2 };
28079 * })
28080 * .directive('convertToNumber', function() {
28081 * return {
28082 * require: 'ngModel',
28083 * link: function(scope, element, attrs, ngModel) {
28084 * ngModel.$parsers.push(function(val) {
28085 * return parseInt(val, 10);
28086 * });
28087 * ngModel.$formatters.push(function(val) {
28088 * return '' + val;
28089 * });
28090 * }
28091 * };
28092 * });
28093 * </file>
28094 * <file name="protractor.js" type="protractor">
28095 * it('should initialize to model', function() {
28096 * var select = element(by.css('select'));
28097 * expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two');
28098 * });
28099 * </file>
28100 * </example>
28101 *
2141428102 */
21415
21416 var ngOptionsDirective = valueFn({ terminal: true });
21417 // jshint maxlen: false
21418 var selectDirective = ['$compile', '$parse', function($compile, $parse) {
21419 //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
21420 var NG_OPTIONS_REGEXP = /^\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]+?))?$/,
21421 nullModelCtrl = {$setViewValue: noop};
21422 // jshint maxlen: 100
28103 var selectDirective = function() {
2142328104
2142428105 return {
2142528106 restrict: 'E',
2142628107 require: ['select', '?ngModel'],
21427 controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
21428 var self = this,
21429 optionsMap = {},
21430 ngModelCtrl = nullModelCtrl,
21431 nullOption,
21432 unknownOption;
21433
21434
21435 self.databound = $attrs.ngModel;
21436
21437
21438 self.init = function(ngModelCtrl_, nullOption_, unknownOption_) {
21439 ngModelCtrl = ngModelCtrl_;
21440 nullOption = nullOption_;
21441 unknownOption = unknownOption_;
28108 controller: SelectController,
28109 link: function(scope, element, attr, ctrls) {
28110
28111 // if ngModel is not defined, we don't need to do anything
28112 var ngModelCtrl = ctrls[1];
28113 if (!ngModelCtrl) return;
28114
28115 var selectCtrl = ctrls[0];
28116
28117 selectCtrl.ngModelCtrl = ngModelCtrl;
28118
28119 // We delegate rendering to the `writeValue` method, which can be changed
28120 // if the select can have multiple selected values or if the options are being
28121 // generated by `ngOptions`
28122 ngModelCtrl.$render = function() {
28123 selectCtrl.writeValue(ngModelCtrl.$viewValue);
2144228124 };
2144328125
21444
21445 self.addOption = function(value) {
21446 assertNotHasOwnProperty(value, '"option value"');
21447 optionsMap[value] = true;
21448
21449 if (ngModelCtrl.$viewValue == value) {
21450 $element.val(value);
21451 if (unknownOption.parent()) unknownOption.remove();
21452 }
21453 };
21454
21455
21456 self.removeOption = function(value) {
21457 if (this.hasOption(value)) {
21458 delete optionsMap[value];
21459 if (ngModelCtrl.$viewValue == value) {
21460 this.renderUnknownOption(value);
28126 // When the selected item(s) changes we delegate getting the value of the select control
28127 // to the `readValue` method, which can be changed if the select can have multiple
28128 // selected values or if the options are being generated by `ngOptions`
28129 element.on('change', function() {
28130 scope.$apply(function() {
28131 ngModelCtrl.$setViewValue(selectCtrl.readValue());
28132 });
28133 });
28134
28135 // If the select allows multiple values then we need to modify how we read and write
28136 // values from and to the control; also what it means for the value to be empty and
28137 // we have to add an extra watch since ngModel doesn't work well with arrays - it
28138 // doesn't trigger rendering if only an item in the array changes.
28139 if (attr.multiple) {
28140
28141 // Read value now needs to check each option to see if it is selected
28142 selectCtrl.readValue = function readMultipleValue() {
28143 var array = [];
28144 forEach(element.find('option'), function(option) {
28145 if (option.selected) {
28146 array.push(option.value);
28147 }
28148 });
28149 return array;
28150 };
28151
28152 // Write value now needs to set the selected property of each matching option
28153 selectCtrl.writeValue = function writeMultipleValue(value) {
28154 var items = new HashMap(value);
28155 forEach(element.find('option'), function(option) {
28156 option.selected = isDefined(items.get(option.value));
28157 });
28158 };
28159
28160 // we have to do it on each watch since ngModel watches reference, but
28161 // we need to work of an array, so we need to see if anything was inserted/removed
28162 var lastView, lastViewRef = NaN;
28163 scope.$watch(function selectMultipleWatch() {
28164 if (lastViewRef === ngModelCtrl.$viewValue && !equals(lastView, ngModelCtrl.$viewValue)) {
28165 lastView = shallowCopy(ngModelCtrl.$viewValue);
28166 ngModelCtrl.$render();
2146128167 }
21462 }
21463 };
21464
21465
21466 self.renderUnknownOption = function(val) {
21467 var unknownVal = '? ' + hashKey(val) + ' ?';
21468 unknownOption.val(unknownVal);
21469 $element.prepend(unknownOption);
21470 $element.val(unknownVal);
21471 unknownOption.prop('selected', true); // needed for IE
21472 };
21473
21474
21475 self.hasOption = function(value) {
21476 return optionsMap.hasOwnProperty(value);
21477 };
21478
21479 $scope.$on('$destroy', function() {
21480 // disable unknown option so that we don't do work when the whole select is being destroyed
21481 self.renderUnknownOption = noop;
21482 });
21483 }],
21484
21485 link: function(scope, element, attr, ctrls) {
21486 // if ngModel is not defined, we don't need to do anything
21487 if (!ctrls[1]) return;
21488
21489 var selectCtrl = ctrls[0],
21490 ngModelCtrl = ctrls[1],
21491 multiple = attr.multiple,
21492 optionsExp = attr.ngOptions,
21493 nullOption = false, // if false, user will not be able to select it (used by ngOptions)
21494 emptyOption,
21495 // we can't just jqLite('<option>') since jqLite is not smart enough
21496 // to create it in <select> and IE barfs otherwise.
21497 optionTemplate = jqLite(document.createElement('option')),
21498 optGroupTemplate =jqLite(document.createElement('optgroup')),
21499 unknownOption = optionTemplate.clone();
21500
21501 // find "null" option
21502 for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) {
21503 if (children[i].value === '') {
21504 emptyOption = nullOption = children.eq(i);
21505 break;
21506 }
21507 }
21508
21509 selectCtrl.init(ngModelCtrl, nullOption, unknownOption);
21510
21511 // required validator
21512 if (multiple) {
28168 lastViewRef = ngModelCtrl.$viewValue;
28169 });
28170
28171 // If we are a multiple select then value is now a collection
28172 // so the meaning of $isEmpty changes
2151328173 ngModelCtrl.$isEmpty = function(value) {
2151428174 return !value || value.length === 0;
2151528175 };
21516 }
21517
21518 if (optionsExp) setupAsOptions(scope, element, ngModelCtrl);
21519 else if (multiple) setupAsMultiple(scope, element, ngModelCtrl);
21520 else setupAsSingle(scope, element, ngModelCtrl, selectCtrl);
21521
21522
21523 ////////////////////////////
21524
21525
21526
21527 function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) {
21528 ngModelCtrl.$render = function() {
21529 var viewValue = ngModelCtrl.$viewValue;
21530
21531 if (selectCtrl.hasOption(viewValue)) {
21532 if (unknownOption.parent()) unknownOption.remove();
21533 selectElement.val(viewValue);
21534 if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy
21535 } else {
21536 if (isUndefined(viewValue) && emptyOption) {
21537 selectElement.val('');
21538 } else {
21539 selectCtrl.renderUnknownOption(viewValue);
21540 }
21541 }
21542 };
21543
21544 selectElement.on('change', function() {
21545 scope.$apply(function() {
21546 if (unknownOption.parent()) unknownOption.remove();
21547 ngModelCtrl.$setViewValue(selectElement.val());
21548 });
21549 });
21550 }
21551
21552 function setupAsMultiple(scope, selectElement, ctrl) {
21553 var lastView;
21554 ctrl.$render = function() {
21555 var items = new HashMap(ctrl.$viewValue);
21556 forEach(selectElement.find('option'), function(option) {
21557 option.selected = isDefined(items.get(option.value));
21558 });
21559 };
21560
21561 // we have to do it on each watch since ngModel watches reference, but
21562 // we need to work of an array, so we need to see if anything was inserted/removed
21563 scope.$watch(function selectMultipleWatch() {
21564 if (!equals(lastView, ctrl.$viewValue)) {
21565 lastView = shallowCopy(ctrl.$viewValue);
21566 ctrl.$render();
21567 }
21568 });
21569
21570 selectElement.on('change', function() {
21571 scope.$apply(function() {
21572 var array = [];
21573 forEach(selectElement.find('option'), function(option) {
21574 if (option.selected) {
21575 array.push(option.value);
21576 }
21577 });
21578 ctrl.$setViewValue(array);
21579 });
21580 });
21581 }
21582
21583 function setupAsOptions(scope, selectElement, ctrl) {
21584 var match;
21585
21586 if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) {
21587 throw ngOptionsMinErr('iexp',
21588 "Expected expression in form of " +
21589 "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
21590 " but got '{0}'. Element: {1}",
21591 optionsExp, startingTag(selectElement));
21592 }
21593
21594 var displayFn = $parse(match[2] || match[1]),
21595 valueName = match[4] || match[6],
21596 keyName = match[5],
21597 groupByFn = $parse(match[3] || ''),
21598 valueFn = $parse(match[2] ? match[1] : valueName),
21599 valuesFn = $parse(match[7]),
21600 track = match[8],
21601 trackFn = track ? $parse(match[8]) : null,
21602 // This is an array of array of existing option groups in DOM.
21603 // We try to reuse these if possible
21604 // - optionGroupsCache[0] is the options with no option group
21605 // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
21606 optionGroupsCache = [[{element: selectElement, label:''}]];
21607
21608 if (nullOption) {
21609 // compile the element since there might be bindings in it
21610 $compile(nullOption)(scope);
21611
21612 // remove the class, which is added automatically because we recompile the element and it
21613 // becomes the compilation root
21614 nullOption.removeClass('ng-scope');
21615
21616 // we need to remove it before calling selectElement.empty() because otherwise IE will
21617 // remove the label from the element. wtf?
21618 nullOption.remove();
21619 }
21620
21621 // clear contents, we'll add what's needed based on the model
21622 selectElement.empty();
21623
21624 selectElement.on('change', function() {
21625 scope.$apply(function() {
21626 var optionGroup,
21627 collection = valuesFn(scope) || [],
21628 locals = {},
21629 key, value, optionElement, index, groupIndex, length, groupLength, trackIndex;
21630
21631 if (multiple) {
21632 value = [];
21633 for (groupIndex = 0, groupLength = optionGroupsCache.length;
21634 groupIndex < groupLength;
21635 groupIndex++) {
21636 // list of options for that group. (first item has the parent)
21637 optionGroup = optionGroupsCache[groupIndex];
21638
21639 for(index = 1, length = optionGroup.length; index < length; index++) {
21640 if ((optionElement = optionGroup[index].element)[0].selected) {
21641 key = optionElement.val();
21642 if (keyName) locals[keyName] = key;
21643 if (trackFn) {
21644 for (trackIndex = 0; trackIndex < collection.length; trackIndex++) {
21645 locals[valueName] = collection[trackIndex];
21646 if (trackFn(scope, locals) == key) break;
21647 }
21648 } else {
21649 locals[valueName] = collection[key];
21650 }
21651 value.push(valueFn(scope, locals));
21652 }
21653 }
21654 }
21655 } else {
21656 key = selectElement.val();
21657 if (key == '?') {
21658 value = undefined;
21659 } else if (key === ''){
21660 value = null;
21661 } else {
21662 if (trackFn) {
21663 for (trackIndex = 0; trackIndex < collection.length; trackIndex++) {
21664 locals[valueName] = collection[trackIndex];
21665 if (trackFn(scope, locals) == key) {
21666 value = valueFn(scope, locals);
21667 break;
21668 }
21669 }
21670 } else {
21671 locals[valueName] = collection[key];
21672 if (keyName) locals[keyName] = key;
21673 value = valueFn(scope, locals);
21674 }
21675 }
21676 }
21677 ctrl.$setViewValue(value);
21678 render();
21679 });
21680 });
21681
21682 ctrl.$render = render;
21683
21684 scope.$watchCollection(valuesFn, render);
21685 if ( multiple ) {
21686 scope.$watchCollection(function() { return ctrl.$modelValue; }, render);
21687 }
21688
21689 function getSelectedSet() {
21690 var selectedSet = false;
21691 if (multiple) {
21692 var modelValue = ctrl.$modelValue;
21693 if (trackFn && isArray(modelValue)) {
21694 selectedSet = new HashMap([]);
21695 var locals = {};
21696 for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) {
21697 locals[valueName] = modelValue[trackIndex];
21698 selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]);
21699 }
21700 } else {
21701 selectedSet = new HashMap(modelValue);
21702 }
21703 }
21704 return selectedSet;
21705 }
21706
21707
21708 function render() {
21709 // Temporary location for the option groups before we render them
21710 var optionGroups = {'':[]},
21711 optionGroupNames = [''],
21712 optionGroupName,
21713 optionGroup,
21714 option,
21715 existingParent, existingOptions, existingOption,
21716 modelValue = ctrl.$modelValue,
21717 values = valuesFn(scope) || [],
21718 keys = keyName ? sortedKeys(values) : values,
21719 key,
21720 groupLength, length,
21721 groupIndex, index,
21722 locals = {},
21723 selected,
21724 selectedSet = getSelectedSet(),
21725 lastElement,
21726 element,
21727 label;
21728
21729
21730 // We now build up the list of options we need (we merge later)
21731 for (index = 0; length = keys.length, index < length; index++) {
21732
21733 key = index;
21734 if (keyName) {
21735 key = keys[index];
21736 if ( key.charAt(0) === '$' ) continue;
21737 locals[keyName] = key;
21738 }
21739
21740 locals[valueName] = values[key];
21741
21742 optionGroupName = groupByFn(scope, locals) || '';
21743 if (!(optionGroup = optionGroups[optionGroupName])) {
21744 optionGroup = optionGroups[optionGroupName] = [];
21745 optionGroupNames.push(optionGroupName);
21746 }
21747 if (multiple) {
21748 selected = isDefined(
21749 selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals))
21750 );
21751 } else {
21752 if (trackFn) {
21753 var modelCast = {};
21754 modelCast[valueName] = modelValue;
21755 selected = trackFn(scope, modelCast) === trackFn(scope, locals);
21756 } else {
21757 selected = modelValue === valueFn(scope, locals);
21758 }
21759 selectedSet = selectedSet || selected; // see if at least one item is selected
21760 }
21761 label = displayFn(scope, locals); // what will be seen by the user
21762
21763 // doing displayFn(scope, locals) || '' overwrites zero values
21764 label = isDefined(label) ? label : '';
21765 optionGroup.push({
21766 // either the index into array or key from object
21767 id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index),
21768 label: label,
21769 selected: selected // determine if we should be selected
21770 });
21771 }
21772 if (!multiple) {
21773 if (nullOption || modelValue === null) {
21774 // insert null option if we have a placeholder, or the model is null
21775 optionGroups[''].unshift({id:'', label:'', selected:!selectedSet});
21776 } else if (!selectedSet) {
21777 // option could not be found, we have to insert the undefined item
21778 optionGroups[''].unshift({id:'?', label:'', selected:true});
21779 }
21780 }
21781
21782 // Now we need to update the list of DOM nodes to match the optionGroups we computed above
21783 for (groupIndex = 0, groupLength = optionGroupNames.length;
21784 groupIndex < groupLength;
21785 groupIndex++) {
21786 // current option group name or '' if no group
21787 optionGroupName = optionGroupNames[groupIndex];
21788
21789 // list of options for that group. (first item has the parent)
21790 optionGroup = optionGroups[optionGroupName];
21791
21792 if (optionGroupsCache.length <= groupIndex) {
21793 // we need to grow the optionGroups
21794 existingParent = {
21795 element: optGroupTemplate.clone().attr('label', optionGroupName),
21796 label: optionGroup.label
21797 };
21798 existingOptions = [existingParent];
21799 optionGroupsCache.push(existingOptions);
21800 selectElement.append(existingParent.element);
21801 } else {
21802 existingOptions = optionGroupsCache[groupIndex];
21803 existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
21804
21805 // update the OPTGROUP label if not the same.
21806 if (existingParent.label != optionGroupName) {
21807 existingParent.element.attr('label', existingParent.label = optionGroupName);
21808 }
21809 }
21810
21811 lastElement = null; // start at the beginning
21812 for(index = 0, length = optionGroup.length; index < length; index++) {
21813 option = optionGroup[index];
21814 if ((existingOption = existingOptions[index+1])) {
21815 // reuse elements
21816 lastElement = existingOption.element;
21817 if (existingOption.label !== option.label) {
21818 lastElement.text(existingOption.label = option.label);
21819 }
21820 if (existingOption.id !== option.id) {
21821 lastElement.val(existingOption.id = option.id);
21822 }
21823 // lastElement.prop('selected') provided by jQuery has side-effects
21824 if (lastElement[0].selected !== option.selected) {
21825 lastElement.prop('selected', (existingOption.selected = option.selected));
21826 if (msie) {
21827 // See #7692
21828 // The selected item wouldn't visually update on IE without this.
21829 // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well
21830 lastElement.prop('selected', existingOption.selected);
21831 }
21832 }
21833 } else {
21834 // grow elements
21835
21836 // if it's a null option
21837 if (option.id === '' && nullOption) {
21838 // put back the pre-compiled element
21839 element = nullOption;
21840 } else {
21841 // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
21842 // in this version of jQuery on some browser the .text() returns a string
21843 // rather then the element.
21844 (element = optionTemplate.clone())
21845 .val(option.id)
21846 .prop('selected', option.selected)
21847 .attr('selected', option.selected)
21848 .text(option.label);
21849 }
21850
21851 existingOptions.push(existingOption = {
21852 element: element,
21853 label: option.label,
21854 id: option.id,
21855 selected: option.selected
21856 });
21857 if (lastElement) {
21858 lastElement.after(element);
21859 } else {
21860 existingParent.element.append(element);
21861 }
21862 lastElement = element;
21863 }
21864 }
21865 // remove any excessive OPTIONs in a group
21866 index++; // increment since the existingOptions[0] is parent element not OPTION
21867 while(existingOptions.length > index) {
21868 existingOptions.pop().element.remove();
21869 }
21870 }
21871 // remove any excessive OPTGROUPs from select
21872 while(optionGroupsCache.length > groupIndex) {
21873 optionGroupsCache.pop()[0].element.remove();
21874 }
21875 }
28176
2187628177 }
2187728178 }
2187828179 };
21879 }];
21880
28180 };
28181
28182
28183 // The option directive is purely designed to communicate the existence (or lack of)
28184 // of dynamically created (and destroyed) option elements to their containing select
28185 // directive via its controller.
2188128186 var optionDirective = ['$interpolate', function($interpolate) {
21882 var nullSelectCtrl = {
21883 addOption: noop,
21884 removeOption: noop
21885 };
28187
28188 function chromeHack(optionElement) {
28189 // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
28190 // Adding an <option selected="selected"> element to a <select required="required"> should
28191 // automatically select the new element
28192 if (optionElement[0].hasAttribute('selected')) {
28193 optionElement[0].selected = true;
28194 }
28195 }
2188628196
2188728197 return {
2188828198 restrict: 'E',
2188928199 priority: 100,
2189028200 compile: function(element, attr) {
28201
28202 // If the value attribute is not defined then we fall back to the
28203 // text content of the option element, which may be interpolated
2189128204 if (isUndefined(attr.value)) {
2189228205 var interpolateFn = $interpolate(element.text(), true);
2189328206 if (!interpolateFn) {
2189528208 }
2189628209 }
2189728210
21898 return function (scope, element, attr) {
28211 return function(scope, element, attr) {
28212
28213 // This is an optimization over using ^^ since we don't want to have to search
28214 // all the way to the root of the DOM for every single option element
2189928215 var selectCtrlName = '$selectController',
2190028216 parent = element.parent(),
2190128217 selectCtrl = parent.data(selectCtrlName) ||
2190228218 parent.parent().data(selectCtrlName); // in case we are in optgroup
2190328219
21904 if (selectCtrl && selectCtrl.databound) {
21905 // For some reason Opera defaults to true and if not overridden this messes up the repeater.
21906 // We don't want the view to drive the initialization of the model anyway.
21907 element.prop('selected', false);
21908 } else {
21909 selectCtrl = nullSelectCtrl;
28220 // Only update trigger option updates if this is an option within a `select`
28221 // that also has `ngModel` attached
28222 if (selectCtrl && selectCtrl.ngModelCtrl) {
28223
28224 if (interpolateFn) {
28225 scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
28226 attr.$set('value', newVal);
28227 if (oldVal !== newVal) {
28228 selectCtrl.removeOption(oldVal);
28229 }
28230 selectCtrl.addOption(newVal, element);
28231 selectCtrl.ngModelCtrl.$render();
28232 chromeHack(element);
28233 });
28234 } else {
28235 selectCtrl.addOption(attr.value, element);
28236 selectCtrl.ngModelCtrl.$render();
28237 chromeHack(element);
28238 }
28239
28240 element.on('$destroy', function() {
28241 selectCtrl.removeOption(attr.value);
28242 selectCtrl.ngModelCtrl.$render();
28243 });
2191028244 }
21911
21912 if (interpolateFn) {
21913 scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
21914 attr.$set('value', newVal);
21915 if (newVal !== oldVal) selectCtrl.removeOption(oldVal);
21916 selectCtrl.addOption(newVal);
21917 });
21918 } else {
21919 selectCtrl.addOption(attr.value);
21920 }
21921
21922 element.on('$destroy', function() {
21923 selectCtrl.removeOption(attr.value);
21924 });
2192528245 };
2192628246 }
2192728247 };
2192928249
2193028250 var styleDirective = valueFn({
2193128251 restrict: 'E',
21932 terminal: true
28252 terminal: false
2193328253 });
28254
28255 var requiredDirective = function() {
28256 return {
28257 restrict: 'A',
28258 require: '?ngModel',
28259 link: function(scope, elm, attr, ctrl) {
28260 if (!ctrl) return;
28261 attr.required = true; // force truthy in case we are on non input element
28262
28263 ctrl.$validators.required = function(modelValue, viewValue) {
28264 return !attr.required || !ctrl.$isEmpty(viewValue);
28265 };
28266
28267 attr.$observe('required', function() {
28268 ctrl.$validate();
28269 });
28270 }
28271 };
28272 };
28273
28274
28275 var patternDirective = function() {
28276 return {
28277 restrict: 'A',
28278 require: '?ngModel',
28279 link: function(scope, elm, attr, ctrl) {
28280 if (!ctrl) return;
28281
28282 var regexp, patternExp = attr.ngPattern || attr.pattern;
28283 attr.$observe('pattern', function(regex) {
28284 if (isString(regex) && regex.length > 0) {
28285 regex = new RegExp('^' + regex + '$');
28286 }
28287
28288 if (regex && !regex.test) {
28289 throw minErr('ngPattern')('noregexp',
28290 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
28291 regex, startingTag(elm));
28292 }
28293
28294 regexp = regex || undefined;
28295 ctrl.$validate();
28296 });
28297
28298 ctrl.$validators.pattern = function(value) {
28299 return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value);
28300 };
28301 }
28302 };
28303 };
28304
28305
28306 var maxlengthDirective = function() {
28307 return {
28308 restrict: 'A',
28309 require: '?ngModel',
28310 link: function(scope, elm, attr, ctrl) {
28311 if (!ctrl) return;
28312
28313 var maxlength = -1;
28314 attr.$observe('maxlength', function(value) {
28315 var intVal = toInt(value);
28316 maxlength = isNaN(intVal) ? -1 : intVal;
28317 ctrl.$validate();
28318 });
28319 ctrl.$validators.maxlength = function(modelValue, viewValue) {
28320 return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
28321 };
28322 }
28323 };
28324 };
28325
28326 var minlengthDirective = function() {
28327 return {
28328 restrict: 'A',
28329 require: '?ngModel',
28330 link: function(scope, elm, attr, ctrl) {
28331 if (!ctrl) return;
28332
28333 var minlength = 0;
28334 attr.$observe('minlength', function(value) {
28335 minlength = toInt(value) || 0;
28336 ctrl.$validate();
28337 });
28338 ctrl.$validators.minlength = function(modelValue, viewValue) {
28339 return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
28340 };
28341 }
28342 };
28343 };
2193428344
2193528345 if (window.angular.bootstrap) {
2193628346 //AngularJS is already loaded, so we can return here...
2193828348 return;
2193928349 }
2194028350
21941 //try to bind to jquery now so that one can write angular.element().read()
28351 //try to bind to jquery now so that one can write jqLite(document).ready()
2194228352 //but we will rebind on bootstrap again.
2194328353 bindJQuery();
2194428354
2195028360
2195128361 })(window, document);
2195228362
21953 !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>');
28363 !window.angular.$$csp() && window.angular.element(document.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-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
00 /*
1 AngularJS v1.2.23
2 (c) 2010-2014 Google, Inc. http://angularjs.org
1 AngularJS v1.4.3
2 (c) 2010-2015 Google, Inc. http://angularjs.org
33 License: MIT
44 */
5 (function(Q,X,t){'use strict';function x(b){return function(){var a=arguments[0],c,a="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.2.23/"+(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 fb(b){if(null==b||Fa(b))return!1;
6 var a=b.length;return 1===b.nodeType&&a?!0:z(b)||H(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function r(b,a,c){var d;if(b)if(P(b))for(d in b)"prototype"==d||("length"==d||"name"==d||b.hasOwnProperty&&!b.hasOwnProperty(d))||a.call(c,b[d],d);else if(H(b)||fb(b))for(d=0;d<b.length;d++)a.call(c,b[d],d);else if(b.forEach&&b.forEach!==r)b.forEach(a,c);else for(d in b)b.hasOwnProperty(d)&&a.call(c,b[d],d);return b}function Zb(b){var a=[],c;for(c in b)b.hasOwnProperty(c)&&a.push(c);return a.sort()}function Tc(b,
7 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 gb(){for(var b=la.length,a;b;){b--;a=la[b].charCodeAt(0);if(57==a)return la[b]="A",la.join("");if(90==a)la[b]="0";else return la[b]=String.fromCharCode(a+1),la.join("")}la.unshift("0");return la.join("")}function ac(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function B(b){var a=b.$$hashKey;r(arguments,function(a){a!==b&&r(a,function(a,c){b[c]=a})});ac(b,a);return b}function Z(b){return parseInt(b,
8 10)}function bc(b,a){return B(new (B(function(){},{prototype:b})),a)}function y(){}function Ga(b){return b}function $(b){return function(){return b}}function D(b){return"undefined"===typeof b}function A(b){return"undefined"!==typeof b}function T(b){return null!=b&&"object"===typeof b}function z(b){return"string"===typeof b}function Ab(b){return"number"===typeof b}function sa(b){return"[object Date]"===ya.call(b)}function P(b){return"function"===typeof b}function hb(b){return"[object RegExp]"===ya.call(b)}
9 function Fa(b){return b&&b.document&&b.location&&b.alert&&b.setInterval}function Uc(b){return!(!b||!(b.nodeName||b.prop&&b.attr&&b.find))}function Vc(b,a,c){var d=[];r(b,function(b,f,g){d.push(a.call(c,b,f,g))});return d}function Qa(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 Ra(b,a){var c=Qa(b,a);0<=c&&b.splice(c,1);return a}function Ha(b,a,c,d){if(Fa(b)||b&&b.$evalAsync&&b.$watch)throw Sa("cpws");if(a){if(b===a)throw Sa("cpi");c=c||[];
10 d=d||[];if(T(b)){var e=Qa(c,b);if(-1!==e)return d[e];c.push(b);d.push(a)}if(H(b))for(var f=a.length=0;f<b.length;f++)e=Ha(b[f],null,c,d),T(b[f])&&(c.push(b[f]),d.push(e)),a.push(e);else{var g=a.$$hashKey;H(a)?a.length=0:r(a,function(b,c){delete a[c]});for(f in b)e=Ha(b[f],null,c,d),T(b[f])&&(c.push(b[f]),d.push(e)),a[f]=e;ac(a,g)}}else if(a=b)H(b)?a=Ha(b,[],c,d):sa(b)?a=new Date(b.getTime()):hb(b)?(a=RegExp(b.source,b.toString().match(/[^\/]*$/)[0]),a.lastIndex=b.lastIndex):T(b)&&(a=Ha(b,{},c,d));
11 return a}function ga(b,a){if(H(b)){a=a||[];for(var c=0;c<b.length;c++)a[c]=b[c]}else if(T(b))for(c in a=a||{},b)!ib.call(b,c)||"$"===c.charAt(0)&&"$"===c.charAt(1)||(a[c]=b[c]);return a||b}function za(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(H(b)){if(!H(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!za(b[d],a[d]))return!1;return!0}}else{if(sa(b))return sa(a)?isNaN(b.getTime())&&isNaN(a.getTime())||b.getTime()===
12 a.getTime():!1;if(hb(b)&&hb(a))return b.toString()==a.toString();if(b&&b.$evalAsync&&b.$watch||a&&a.$evalAsync&&a.$watch||Fa(b)||Fa(a)||H(a))return!1;c={};for(d in b)if("$"!==d.charAt(0)&&!P(b[d])){if(!za(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!c.hasOwnProperty(d)&&"$"!==d.charAt(0)&&a[d]!==t&&!P(a[d]))return!1;return!0}return!1}function Bb(b,a){var c=2<arguments.length?Aa.call(arguments,2):[];return!P(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,c.concat(Aa.call(arguments,
13 0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function Wc(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)?c=t:Fa(a)?c="$WINDOW":a&&X===a?c="$DOCUMENT":a&&(a.$evalAsync&&a.$watch)&&(c="$SCOPE");return c}function ta(b,a){return"undefined"===typeof b?t:JSON.stringify(b,Wc,a?" ":null)}function cc(b){return z(b)?JSON.parse(b):b}function Ta(b){"function"===typeof b?b=!0:b&&0!==b.length?(b=N(""+b),b=!("f"==b||"0"==b||"false"==b||"no"==b||"n"==b||"[]"==b)):b=!1;
14 return b}function ha(b){b=u(b).clone();try{b.empty()}catch(a){}var c=u("<div>").append(b).html();try{return 3===b[0].nodeType?N(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+N(b)})}catch(d){return N(c)}}function dc(b){try{return decodeURIComponent(b)}catch(a){}}function ec(b){var a={},c,d;r((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g,"%20").split("="),d=dc(c[0]),A(d)&&(b=A(c[1])?dc(c[1]):!0,ib.call(a,d)?H(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Cb(b){var a=
15 [];r(b,function(b,d){H(b)?r(b,function(b){a.push(Ba(d,!0)+(!0===b?"":"="+Ba(b,!0)))}):a.push(Ba(d,!0)+(!0===b?"":"="+Ba(b,!0)))});return a.length?a.join("&"):""}function jb(b){return Ba(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Ba(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function Xc(b,a){function c(a){a&&d.push(a)}var d=[b],e,f,g=["ng:app","ng-app","x-ng-app",
16 "data-ng-app"],k=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;r(g,function(a){g[a]=!0;c(X.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(r(b.querySelectorAll("."+a),c),r(b.querySelectorAll("."+a+"\\:"),c),r(b.querySelectorAll("["+a+"]"),c))});r(d,function(a){if(!e){var b=k.exec(" "+a.className+" ");b?(e=a,f=(b[2]||"").replace(/\s+/g,",")):r(a.attributes,function(b){!e&&g[b.name]&&(e=a,f=b.value)})}});e&&a(e,f?[f]:[])}function fc(b,a){var c=function(){b=u(b);if(b.injector()){var c=b[0]===X?
17 "document":ha(b);throw Sa("btstrpd",c.replace(/</,"&lt;").replace(/>/,"&gt;"));}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");c=gc(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(Q&&!d.test(Q.name))return c();Q.name=Q.name.replace(d,"");Ua.resumeBootstrap=function(b){r(b,function(b){a.push(b)});c()}}function kb(b,a){a=
18 a||"_";return b.replace(Yc,function(b,d){return(d?a:"")+b.toLowerCase()})}function Db(b,a,c){if(!b)throw Sa("areq",a||"?",c||"required");return b}function Va(b,a,c){c&&H(b)&&(b=b[b.length-1]);Db(P(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ca(b,a){if("hasOwnProperty"===b)throw Sa("badname",a);}function hc(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&&P(b)?Bb(e,b):b}function Eb(b){var a=
19 b[0];b=b[b.length-1];if(a===b)return u(a);var c=[a];do{a=a.nextSibling;if(!a)break;c.push(a)}while(a!==b);return u(c)}function Zc(b){var a=x("$injector"),c=x("ng");b=b.angular||(b.angular={});b.$$minErr=b.$$minErr||x;return b.module||(b.module=function(){var b={};return function(e,f,g){if("hasOwnProperty"===e)throw c("badname","module");f&&b.hasOwnProperty(e)&&(b[e]=null);return b[e]||(b[e]=function(){function b(a,d,e){return function(){c[e||"push"]([a,d,arguments]);return n}}if(!f)throw a("nomod",
20 e);var c=[],d=[],l=b("$injector","invoke"),n={_invokeQueue:c,_runBlocks:d,requires:f,name:e,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:b("$provide","value"),constant:b("$provide","constant","unshift"),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider","directive"),config:l,run:function(a){d.push(a);return this}};g&&l(g);return n}())}}())}
21 function $c(b){B(b,{bootstrap:fc,copy:Ha,extend:B,equals:za,element:u,forEach:r,injector:gc,noop:y,bind:Bb,toJson:ta,fromJson:cc,identity:Ga,isUndefined:D,isDefined:A,isString:z,isFunction:P,isObject:T,isNumber:Ab,isElement:Uc,isArray:H,version:ad,isDate:sa,lowercase:N,uppercase:Ia,callbacks:{counter:0},$$minErr:x,$$csp:Wa});Xa=Zc(Q);try{Xa("ngLocale")}catch(a){Xa("ngLocale",[]).provider("$locale",bd)}Xa("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:cd});a.provider("$compile",
22 ic).directive({a:dd,input:jc,textarea:jc,form:ed,script:fd,select:gd,style:hd,option:id,ngBind:jd,ngBindHtml:kd,ngBindTemplate:ld,ngClass:md,ngClassEven:nd,ngClassOdd:od,ngCloak:pd,ngController:qd,ngForm:rd,ngHide:sd,ngIf:td,ngInclude:ud,ngInit:vd,ngNonBindable:wd,ngPluralize:xd,ngRepeat:yd,ngShow:zd,ngStyle:Ad,ngSwitch:Bd,ngSwitchWhen:Cd,ngSwitchDefault:Dd,ngOptions:Ed,ngTransclude:Fd,ngModel:Gd,ngList:Hd,ngChange:Id,required:kc,ngRequired:kc,ngValue:Jd}).directive({ngInclude:Kd}).directive(Fb).directive(lc);
23 a.provider({$anchorScroll:Ld,$animate:Md,$browser:Nd,$cacheFactory:Od,$controller:Pd,$document:Qd,$exceptionHandler:Rd,$filter:mc,$interpolate:Sd,$interval:Td,$http:Ud,$httpBackend:Vd,$location:Wd,$log:Xd,$parse:Yd,$rootScope:Zd,$q:$d,$sce:ae,$sceDelegate:be,$sniffer:ce,$templateCache:de,$timeout:ee,$window:fe,$$rAF:ge,$$asyncCallback:he})}])}function Ya(b){return b.replace(ie,function(a,b,d,e){return e?d.toUpperCase():d}).replace(je,"Moz$1")}function Gb(b,a,c,d){function e(b){var e=c&&b?[this.filter(b)]:
24 [this],m=a,h,l,n,p,q,s;if(!d||null!=b)for(;e.length;)for(h=e.shift(),l=0,n=h.length;l<n;l++)for(p=u(h[l]),m?p.triggerHandler("$destroy"):m=!m,q=0,p=(s=p.children()).length;q<p;q++)e.push(Da(s[q]));return f.apply(this,arguments)}var f=Da.fn[b],f=f.$original||f;e.$original=f;Da.fn[b]=e}function S(b){if(b instanceof S)return b;z(b)&&(b=aa(b));if(!(this instanceof S)){if(z(b)&&"<"!=b.charAt(0))throw Hb("nosel");return new S(b)}if(z(b)){var a=b;b=X;var c;if(c=ke.exec(a))b=[b.createElement(c[1])];else{var d=
25 b,e;b=d.createDocumentFragment();c=[];if(Ib.test(a)){d=b.appendChild(d.createElement("div"));e=(le.exec(a)||["",""])[1].toLowerCase();e=ba[e]||ba._default;d.innerHTML="<div>&#160;</div>"+e[1]+a.replace(me,"<$1></$2>")+e[2];d.removeChild(d.firstChild);for(a=e[0];a--;)d=d.lastChild;a=0;for(e=d.childNodes.length;a<e;++a)c.push(d.childNodes[a]);d=b.firstChild;d.textContent=""}else c.push(d.createTextNode(a));b.textContent="";b.innerHTML="";b=c}Jb(this,b);u(X.createDocumentFragment()).append(this)}else Jb(this,
26 b)}function Kb(b){return b.cloneNode(!0)}function Ja(b){Lb(b);var a=0;for(b=b.childNodes||[];a<b.length;a++)Ja(b[a])}function nc(b,a,c,d){if(A(d))throw Hb("offargs");var e=ma(b,"events");ma(b,"handle")&&(D(a)?r(e,function(a,c){Za(b,c,a);delete e[c]}):r(a.split(" "),function(a){D(c)?(Za(b,a,e[a]),delete e[a]):Ra(e[a]||[],c)}))}function Lb(b,a){var c=b.ng339,d=$a[c];d&&(a?delete $a[c].data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),nc(b)),delete $a[c],b.ng339=t))}function ma(b,a,c){var d=
27 b.ng339,d=$a[d||-1];if(A(c))d||(b.ng339=d=++ne,d=$a[d]={}),d[a]=c;else return d&&d[a]}function Mb(b,a,c){var d=ma(b,"data"),e=A(c),f=!e&&A(a),g=f&&!T(a);d||g||ma(b,"data",d={});if(e)d[a]=c;else if(f){if(g)return d&&d[a];B(d,a)}else return d}function Nb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function lb(b,a){a&&b.setAttribute&&r(a.split(" "),function(a){b.setAttribute("class",aa((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g,
28 " ").replace(" "+aa(a)+" "," ")))})}function mb(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");r(a.split(" "),function(a){a=aa(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",aa(c))}}function Jb(b,a){if(a){a=a.nodeName||!A(a.length)||Fa(a)?[a]:a;for(var c=0;c<a.length;c++)b.push(a[c])}}function oc(b,a){return nb(b,"$"+(a||"ngController")+"Controller")}function nb(b,a,c){9==b.nodeType&&(b=b.documentElement);for(a=H(a)?a:[a];b;){for(var d=
29 0,e=a.length;d<e;d++)if((c=u.data(b,a[d]))!==t)return c;b=b.parentNode||11===b.nodeType&&b.host}}function pc(b){for(var a=0,c=b.childNodes;a<c.length;a++)Ja(c[a]);for(;b.firstChild;)b.removeChild(b.firstChild)}function qc(b,a){var c=ob[a.toLowerCase()];return c&&rc[b.nodeName]&&c}function oe(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||X);if(D(c.defaultPrevented)){var f=
30 c.preventDefault;c.preventDefault=function(){c.defaultPrevented=!0;f.call(c)};c.defaultPrevented=!1}c.isDefaultPrevented=function(){return c.defaultPrevented||!1===c.returnValue};var g=ga(a[e||c.type]||[]);r(g,function(a){a.call(b,c)});8>=R?(c.preventDefault=null,c.stopPropagation=null,c.isDefaultPrevented=null):(delete c.preventDefault,delete c.stopPropagation,delete c.isDefaultPrevented)};c.elem=b;return c}function Ka(b,a){var c=typeof b,d;"function"==c||"object"==c&&null!==b?"function"==typeof(d=
31 b.$$hashKey)?d=b.$$hashKey():d===t&&(d=b.$$hashKey=(a||gb)()):d=b;return c+":"+d}function ab(b,a){if(a){var c=0;this.nextUid=function(){return++c}}r(b,this.put,this)}function sc(b){var a,c;"function"===typeof b?(a=b.$inject)||(a=[],b.length&&(c=b.toString().replace(pe,""),c=c.match(qe),r(c[1].split(re),function(b){b.replace(se,function(b,c,d){a.push(d)})})),b.$inject=a):H(b)?(c=b.length-1,Va(b[c],"fn"),a=b.slice(0,c)):Va(b,"fn",!0);return a}function gc(b){function a(a){return function(b,c){if(T(b))r(b,
32 $b(a));else return a(b,c)}}function c(a,b){Ca(a,"service");if(P(b)||H(b))b=n.instantiate(b);if(!b.$get)throw bb("pget",a);return l[a+k]=b}function d(a,b){return c(a,{$get:b})}function e(a){var b=[],c,d,f,k;r(a,function(a){if(!h.get(a)){h.put(a,!0);try{if(z(a))for(c=Xa(a),b=b.concat(e(c.requires)).concat(c._runBlocks),d=c._invokeQueue,f=0,k=d.length;f<k;f++){var g=d[f],m=n.get(g[0]);m[g[1]].apply(m,g[2])}else P(a)?b.push(n.invoke(a)):H(a)?b.push(n.invoke(a)):Va(a,"module")}catch(l){throw H(a)&&(a=
33 a[a.length-1]),l.message&&(l.stack&&-1==l.stack.indexOf(l.message))&&(l=l.message+"\n"+l.stack),bb("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 bb("cdep",d+" <- "+m.join(" <- "));return a[d]}try{return m.unshift(d),a[d]=g,a[d]=b(d)}catch(e){throw a[d]===g&&delete a[d],e;}finally{m.shift()}}function d(a,b,e){var f=[],k=sc(a),g,m,h;m=0;for(g=k.length;m<g;m++){h=k[m];if("string"!==typeof h)throw bb("itkn",h);f.push(e&&e.hasOwnProperty(h)?
34 e[h]:c(h))}H(a)&&(a=a[g]);return a.apply(b,f)}return{invoke:d,instantiate:function(a,b){var c=function(){},e;c.prototype=(H(a)?a[a.length-1]:a).prototype;c=new c;e=d(a,c,b);return T(e)||P(e)?e:c},get:c,annotate:sc,has:function(b){return l.hasOwnProperty(b+k)||a.hasOwnProperty(b)}}}var g={},k="Provider",m=[],h=new ab([],!0),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,$(b))}),constant:a(function(a,
35 b){Ca(a,"constant");l[a]=b;p[a]=b}),decorator:function(a,b){var c=n.get(a+k),d=c.$get;c.$get=function(){var a=q.invoke(d,c);return q.invoke(b,null,{$delegate:a})}}}},n=l.$injector=f(l,function(){throw bb("unpr",m.join(" <- "));}),p={},q=p.$injector=f(p,function(a){a=n.get(a+k);return q.invoke(a.$get,a)});r(e(b),function(a){q.invoke(a||y)});return q}function Ld(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;
36 r(a,function(a){b||"a"!==N(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 he(){this.$get=["$$rAF","$timeout",function(b,a){return b.supported?function(a){return b(a)}:function(b){return a(b,0,!1)}}]}function te(b,a,c,d){function e(a){try{a.apply(null,
37 Aa.call(arguments,1))}finally{if(s--,0===s)for(;L.length;)try{L.pop()()}catch(b){c.error(b)}}}function f(a,b){(function ca(){r(v,function(a){a()});C=b(ca,a)})()}function g(){w=null;O!=k.url()&&(O=k.url(),r(da,function(a){a(k.url())}))}var k=this,m=a[0],h=b.location,l=b.history,n=b.setTimeout,p=b.clearTimeout,q={};k.isMock=!1;var s=0,L=[];k.$$completeOutstandingRequest=e;k.$$incOutstandingRequestCount=function(){s++};k.notifyWhenNoOutstandingRequests=function(a){r(v,function(a){a()});0===s?a():L.push(a)};
38 var v=[],C;k.addPollFn=function(a){D(C)&&f(100,n);v.push(a);return a};var O=h.href,I=a.find("base"),w=null;k.url=function(a,c){h!==b.location&&(h=b.location);l!==b.history&&(l=b.history);if(a){if(O!=a)return O=a,d.history?c?l.replaceState(null,"",a):(l.pushState(null,"",a),I.attr("href",I.attr("href"))):(w=a,c?h.replace(a):h.href=a),k}else return w||h.href.replace(/%27/g,"'")};var da=[],K=!1;k.onUrlChange=function(a){if(!K){if(d.history)u(b).on("popstate",g);if(d.hashchange)u(b).on("hashchange",g);
39 else k.addPollFn(g);K=!0}da.push(a);return a};k.baseHref=function(){var a=I.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};var W={},ea="",J=k.baseHref();k.cookies=function(a,b){var d,e,f,k;if(a)b===t?m.cookie=escape(a)+"=;path="+J+";expires=Thu, 01 Jan 1970 00:00:00 GMT":z(b)&&(d=(m.cookie=escape(a)+"="+escape(b)+";path="+J).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!==ea)for(ea=m.cookie,
40 d=ea.split("; "),W={},f=0;f<d.length;f++)e=d[f],k=e.indexOf("="),0<k&&(a=unescape(e.substring(0,k)),W[a]===t&&(W[a]=unescape(e.substring(k+1))));return W}};k.defer=function(a,b){var c;s++;c=n(function(){delete q[c];e(a)},b||0);q[c]=!0;return c};k.defer.cancel=function(a){return q[a]?(delete q[a],p(a),e(y),!0):!1}}function Nd(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new te(b,d,a,c)}]}function Od(){this.$get=function(){function b(b,d){function e(a){a!=n&&(p?p==a&&
41 (p=a.n):p=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 x("$cacheFactory")("iid",b);var g=0,k=B({},d,{id:b}),m={},h=d&&d.capacity||Number.MAX_VALUE,l={},n=null,p=null;return a[b]={put:function(a,b){if(h<Number.MAX_VALUE){var c=l[a]||(l[a]={key:a});e(c)}if(!D(b))return a in m||g++,m[a]=b,g>h&&this.remove(p.key),b},get:function(a){if(h<Number.MAX_VALUE){var b=l[a];if(!b)return;e(b)}return m[a]},remove:function(a){if(h<Number.MAX_VALUE){var b=l[a];if(!b)return;
42 b==n&&(n=b.p);b==p&&(p=b.n);f(b.n,b.p);delete l[a]}delete m[a];g--},removeAll:function(){m={};g=0;l={};n=p=null},destroy:function(){l=k=m=null;delete a[b]},info:function(){return B({},k,{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 de(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function ic(b,a){var c={},d="Directive",e=/^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/,f=/(([\d\w_\-]+)(?:\:([^;]+))?;?)/,
43 g=/^(on[a-z]+|formaction)$/;this.directive=function m(a,e){Ca(a,"directive");z(a)?(Db(e,"directiveFactory"),c.hasOwnProperty(a)||(c[a]=[],b.factory(a+d,["$injector","$exceptionHandler",function(b,d){var e=[];r(c[a],function(c,f){try{var g=b.invoke(c);P(g)?g={compile:$(g)}:!g.compile&&g.link&&(g.compile=$(g.link));g.priority=g.priority||0;g.index=f;g.name=g.name||a;g.require=g.require||g.controller&&g.name;g.restrict=g.restrict||"A";e.push(g)}catch(m){d(m)}});return e}])),c[a].push(e)):r(a,$b(m));
44 return this};this.aHrefSanitizationWhitelist=function(b){return A(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(b){return A(b)?(a.imgSrcSanitizationWhitelist(b),this):a.imgSrcSanitizationWhitelist()};this.$get=["$injector","$interpolate","$exceptionHandler","$http","$templateCache","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri",function(a,b,l,n,p,q,s,L,v,C,O,I){function w(a,b,c,d,e){a instanceof
45 u||(a=u(a));r(a,function(b,c){3==b.nodeType&&b.nodeValue.match(/\S+/)&&(a[c]=u(b).wrap("<span></span>").parent()[0])});var f=K(a,b,a,c,d,e);da(a,"ng-scope");return function(b,c,d,e){Db(b,"scope");var g=c?La.clone.call(a):a;r(d,function(a,b){g.data("$"+b+"Controller",a)});d=0;for(var m=g.length;d<m;d++){var h=g[d].nodeType;1!==h&&9!==h||g.eq(d).data("$scope",b)}c&&c(g,b);f&&f(b,g,g,e);return g}}function da(a,b){try{a.addClass(b)}catch(c){}}function K(a,b,c,d,e,f){function g(a,c,d,e){var f,h,l,q,n,
46 p,s;f=c.length;var M=Array(f);for(q=0;q<f;q++)M[q]=c[q];p=q=0;for(n=m.length;q<n;p++)h=M[p],c=m[q++],f=m[q++],c?(c.scope?(l=a.$new(),u.data(h,"$scope",l)):l=a,s=c.transcludeOnThisElement?W(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?W(a,b):null,c(f,l,h,d,s)):f&&f(a,h.childNodes,t,e)}for(var m=[],h,l,q,n,p=0;p<a.length;p++)h=new Ob,l=ea(a[p],[],h,0===p?d:t,e),(f=l.length?F(l,a[p],h,b,c,null,[],[],f):null)&&f.scope&&da(h.$$element,"ng-scope"),h=f&&f.terminal||!(q=a[p].childNodes)||!q.length?
47 null:K(q,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b),m.push(f,h),n=n||f||h,f=null;return n?g:null}function W(a,b,c){return function(d,e,f){var g=!1;d||(d=a.$new(),g=d.$$transcluded=!0);e=b(d,e,f,c);if(g)e.on("$destroy",function(){d.$destroy()});return e}}function ea(a,b,c,d,g){var h=c.$attr,m;switch(a.nodeType){case 1:ca(b,na(Ma(a).toLowerCase()),"E",d,g);for(var l,q,n,p=a.attributes,s=0,L=p&&p.length;s<L;s++){var C=!1,O=!1;l=p[s];if(!R||8<=R||l.specified){m=l.name;q=
48 aa(l.value);l=na(m);if(n=V.test(l))m=kb(l.substr(6),"-");var v=l.replace(/(Start|End)$/,"");l===v+"Start"&&(C=m,O=m.substr(0,m.length-5)+"end",m=m.substr(0,m.length-6));l=na(m.toLowerCase());h[l]=m;if(n||!c.hasOwnProperty(l))c[l]=q,qc(a,l)&&(c[l]=!0);Q(a,b,q,l);ca(b,l,"A",d,g,C,O)}}a=a.className;if(z(a)&&""!==a)for(;m=f.exec(a);)l=na(m[2]),ca(b,l,"C",d,g)&&(c[l]=aa(m[3])),a=a.substr(m.index+m[0].length);break;case 3:x(b,a.nodeValue);break;case 8:try{if(m=e.exec(a.nodeValue))l=na(m[1]),ca(b,l,"M",
49 d,g)&&(c[l]=aa(m[2]))}catch(w){}}b.sort(D);return b}function J(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw ia("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 u(d)}function E(a,b,c){return function(d,e,f,g,m){e=J(e[0],b,c);return a(d,e,f,g,m)}}function F(a,c,d,e,f,g,m,n,p){function L(a,b,c,d){if(a){c&&(a=E(a,c,d));a.require=G.require;a.directiveName=oa;if(K===G||G.$$isolateScope)a=
50 tc(a,{isolateScope:!0});m.push(a)}if(b){c&&(b=E(b,c,d));b.require=G.require;b.directiveName=oa;if(K===G||G.$$isolateScope)b=tc(b,{isolateScope:!0});n.push(b)}}function C(a,b,c,d){var e,f="data",g=!1;if(z(b)){for(;"^"==(e=b.charAt(0))||"?"==e;)b=b.substr(1),"^"==e&&(f="inheritedData"),g=g||"?"==e;e=null;d&&"data"===f&&(e=d[b]);e=e||c[f]("$"+b+"Controller");if(!e&&!g)throw ia("ctreq",b,a);}else H(b)&&(e=[],r(b,function(b){e.push(C(a,b,c,d))}));return e}function O(a,e,f,g,p){function L(a,b){var c;2>
51 arguments.length&&(b=a,a=t);Ea&&(c=ea);return p(a,b,c)}var v,M,w,I,E,J,ea={},qb;v=c===f?d:ga(d,new Ob(u(f),d.$attr));M=v.$$element;if(K){var Na=/^\s*([@=&])(\??)\s*(\w*)\s*$/;J=e.$new(!0);!F||F!==K&&F!==K.$$originalDirective?M.data("$isolateScopeNoTemplate",J):M.data("$isolateScope",J);da(M,"ng-isolate-scope");r(K.scope,function(a,c){var d=a.match(Na)||[],f=d[3]||c,g="?"==d[2],d=d[1],m,l,n,p;J.$$isolateBindings[c]=d+f;switch(d){case "@":v.$observe(f,function(a){J[c]=a});v.$$observers[f].$$scope=e;
52 v[f]&&(J[c]=b(v[f])(e));break;case "=":if(g&&!v[f])break;l=q(v[f]);p=l.literal?za:function(a,b){return a===b||a!==a&&b!==b};n=l.assign||function(){m=J[c]=l(e);throw ia("nonassign",v[f],K.name);};m=J[c]=l(e);J.$watch(function(){var a=l(e);p(a,J[c])||(p(a,m)?n(e,a=J[c]):J[c]=a);return m=a},null,l.literal);break;case "&":l=q(v[f]);J[c]=function(a){return l(e,a)};break;default:throw ia("iscp",K.name,c,a);}})}qb=p&&L;W&&r(W,function(a){var b={$scope:a===K||a.$$isolateScope?J:e,$element:M,$attrs:v,$transclude:qb},
53 c;E=a.controller;"@"==E&&(E=v[a.name]);c=s(E,b);ea[a.name]=c;Ea||M.data("$"+a.name+"Controller",c);a.controllerAs&&(b.$scope[a.controllerAs]=c)});g=0;for(w=m.length;g<w;g++)try{I=m[g],I(I.isolateScope?J:e,M,v,I.require&&C(I.directiveName,I.require,M,ea),qb)}catch(ca){l(ca,ha(M))}g=e;K&&(K.template||null===K.templateUrl)&&(g=J);a&&a(g,f.childNodes,t,p);for(g=n.length-1;0<=g;g--)try{I=n[g],I(I.isolateScope?J:e,M,v,I.require&&C(I.directiveName,I.require,M,ea),qb)}catch(pb){l(pb,ha(M))}}p=p||{};for(var v=
54 -Number.MAX_VALUE,I,W=p.controllerDirectives,K=p.newIsolateScopeDirective,F=p.templateDirective,ca=p.nonTlbTranscludeDirective,D=!1,B=!1,Ea=p.hasElementTranscludeDirective,x=d.$$element=u(c),G,oa,U,S=e,R,Q=0,pa=a.length;Q<pa;Q++){G=a[Q];var V=G.$$start,Y=G.$$end;V&&(x=J(c,V,Y));U=t;if(v>G.priority)break;if(U=G.scope)I=I||G,G.templateUrl||(N("new/isolated scope",K,G,x),T(U)&&(K=G));oa=G.name;!G.templateUrl&&G.controller&&(U=G.controller,W=W||{},N("'"+oa+"' controller",W[oa],G,x),W[oa]=G);if(U=G.transclude)D=
55 !0,G.$$tlb||(N("transclusion",ca,G,x),ca=G),"element"==U?(Ea=!0,v=G.priority,U=x,x=d.$$element=u(X.createComment(" "+oa+": "+d[oa]+" ")),c=x[0],Na(f,Aa.call(U,0),c),S=w(U,e,v,g&&g.name,{nonTlbTranscludeDirective:ca})):(U=u(Kb(c)).contents(),x.empty(),S=w(U,e));if(G.template)if(B=!0,N("template",F,G,x),F=G,U=P(G.template)?G.template(x,d):G.template,U=Z(U),G.replace){g=G;U=Ib.test(U)?u(aa(U)):[];c=U[0];if(1!=U.length||1!==c.nodeType)throw ia("tplrt",oa,"");Na(f,x,c);pa={$attr:{}};U=ea(c,[],pa);var $=
56 a.splice(Q+1,a.length-(Q+1));K&&pb(U);a=a.concat(U).concat($);A(d,pa);pa=a.length}else x.html(U);if(G.templateUrl)B=!0,N("template",F,G,x),F=G,G.replace&&(g=G),O=y(a.splice(Q,a.length-Q),x,d,f,D&&S,m,n,{controllerDirectives:W,newIsolateScopeDirective:K,templateDirective:F,nonTlbTranscludeDirective:ca}),pa=a.length;else if(G.compile)try{R=G.compile(x,d,S),P(R)?L(null,R,V,Y):R&&L(R.pre,R.post,V,Y)}catch(ba){l(ba,ha(x))}G.terminal&&(O.terminal=!0,v=Math.max(v,G.priority))}O.scope=I&&!0===I.scope;O.transcludeOnThisElement=
57 D;O.templateOnThisElement=B;O.transclude=S;p.hasElementTranscludeDirective=Ea;return O}function pb(a){for(var b=0,c=a.length;b<c;b++)a[b]=bc(a[b],{$$isolateScope:!0})}function ca(b,e,f,g,h,q,n){if(e===h)return null;h=null;if(c.hasOwnProperty(e)){var p;e=a.get(e+d);for(var s=0,v=e.length;s<v;s++)try{p=e[s],(g===t||g>p.priority)&&-1!=p.restrict.indexOf(f)&&(q&&(p=bc(p,{$$start:q,$$end:n})),b.push(p),h=p)}catch(L){l(L)}}return h}function A(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;r(a,function(d,e){"$"!=
58 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?(da(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 y(a,b,c,d,e,f,g,m){var h=[],l,q,s=b[0],v=a.shift(),L=B({},v,{templateUrl:null,transclude:null,replace:null,$$originalDirective:v}),O=P(v.templateUrl)?v.templateUrl(b,c):v.templateUrl;
59 b.empty();n.get(C.getTrustedResourceUrl(O),{cache:p}).success(function(n){var p,C;n=Z(n);if(v.replace){n=Ib.test(n)?u(aa(n)):[];p=n[0];if(1!=n.length||1!==p.nodeType)throw ia("tplrt",v.name,O);n={$attr:{}};Na(d,b,p);var w=ea(p,[],n);T(v.scope)&&pb(w);a=w.concat(a);A(c,n)}else p=s,b.html(n);a.unshift(L);l=F(a,p,c,e,b,v,f,g,m);r(d,function(a,c){a==p&&(d[c]=b[0])});for(q=K(b[0].childNodes,e);h.length;){n=h.shift();C=h.shift();var I=h.shift(),E=h.shift(),w=b[0];if(C!==s){var J=C.className;m.hasElementTranscludeDirective&&
60 v.replace||(w=Kb(p));Na(I,u(C),w);da(u(w),J)}C=l.transcludeOnThisElement?W(n,l.transclude,E):E;l(q,n,w,d,C)}h=null}).error(function(a,b,c,d){throw ia("tpload",d.url);});return function(a,b,c,d,e){a=e;h?(h.push(b),h.push(c),h.push(d),h.push(a)):(l.transcludeOnThisElement&&(a=W(b,l.transclude,e)),l(q,b,c,d,a))}}function D(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 N(a,b,c,d){if(b)throw ia("multidir",b.name,c.name,a,ha(d));}function x(a,
61 c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){var b=a.parent().length;b&&da(a.parent(),"ng-binding");return function(a,c){var e=c.parent(),f=e.data("$binding")||[];f.push(d);e.data("$binding",f);b||da(e,"ng-binding");a.$watch(d,function(a){c[0].nodeValue=a})}}})}function S(a,b){if("srcdoc"==b)return C.HTML;var c=Ma(a);if("xlinkHref"==b||"FORM"==c&&"action"==b||"IMG"!=c&&("src"==b||"ngSrc"==b))return C.RESOURCE_URL}function Q(a,c,d,e){var f=b(d,!0);if(f){if("multiple"===e&&"SELECT"===
62 Ma(a))throw ia("selmulti",ha(a));c.push({priority:100,compile:function(){return{pre:function(c,d,m){d=m.$$observers||(m.$$observers={});if(g.test(e))throw ia("nodomevents");if(f=b(m[e],!0,S(a,e)))m[e]=f(c),(d[e]||(d[e]=[])).$$inter=!0,(m.$$observers&&m.$$observers[e].$$scope||c).$watch(f,function(a,b){"class"===e&&a!=b?m.$updateClass(a,b):m.$set(e,a)})}}}})}}function Na(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,m;if(a)for(g=0,m=a.length;g<m;g++)if(a[g]==d){a[g++]=c;m=g+e-1;for(var h=a.length;g<
63 h;g++,m++)m<h?a[g]=a[m]:delete a[g];a.length-=e-1;break}f&&f.replaceChild(c,d);a=X.createDocumentFragment();a.appendChild(d);c[u.expando]=d[u.expando];d=1;for(e=b.length;d<e;d++)f=b[d],u(f).remove(),a.appendChild(f),delete b[d];b[0]=c;b.length=1}function tc(a,b){return B(function(){return a.apply(null,arguments)},a,b)}var Ob=function(a,b){this.$$element=a;this.$attr=b||{}};Ob.prototype={$normalize:na,$addClass:function(a){a&&0<a.length&&O.addClass(this.$$element,a)},$removeClass:function(a){a&&0<
64 a.length&&O.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=uc(a,b),d=uc(b,a);0===c.length?O.removeClass(this.$$element,d):0===d.length?O.addClass(this.$$element,c):O.setClass(this.$$element,c,d)},$set:function(a,b,c,d){var e=qc(this.$$element[0],a);e&&(this.$$element.prop(a,b),d=e);this[a]=b;d?this.$attr[a]=d:(d=this.$attr[a])||(this.$attr[a]=d=kb(a,"-"));e=Ma(this.$$element);if("A"===e&&"href"===a||"IMG"===e&&"src"===a)this[a]=b=I(b,"src"===a);!1!==c&&(null===b||b===t?this.$$element.removeAttr(d):
65 this.$$element.attr(d,b));(c=this.$$observers)&&r(c[a],function(a){try{a(b)}catch(c){l(c)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers={}),e=d[a]||(d[a]=[]);e.push(b);L.$evalAsync(function(){e.$$inter||b(c[a])});return b}};var pa=b.startSymbol(),Ea=b.endSymbol(),Z="{{"==pa||"}}"==Ea?Ga:function(a){return a.replace(/\{\{/g,pa).replace(/}}/g,Ea)},V=/^ngAttr[A-Z]/;return w}]}function na(b){return Ya(b.replace(ue,""))}function uc(b,a){var c="",d=b.split(/\s+/),e=a.split(/\s+/),
66 f=0;a:for(;f<d.length;f++){for(var g=d[f],k=0;k<e.length;k++)if(g==e[k])continue a;c+=(0<c.length?" ":"")+g}return c}function Pd(){var b={},a=/^(\S+)(\s+as\s+(\w+))?$/;this.register=function(a,d){Ca(a,"controller");T(a)?B(b,a):b[a]=d};this.$get=["$injector","$window",function(c,d){return function(e,f){var g,k,m;z(e)&&(g=e.match(a),k=g[1],m=g[3],e=b.hasOwnProperty(k)?b[k]:hc(f.$scope,k,!0)||hc(d,k,!0),Va(e,k,!0));g=c.instantiate(e,f);if(m){if(!f||"object"!==typeof f.$scope)throw x("$controller")("noscp",
67 k||e.name,m);f.$scope[m]=g}return g}}]}function Qd(){this.$get=["$window",function(b){return u(b.document)}]}function Rd(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function vc(b){var a={},c,d,e;if(!b)return a;r(b.split("\n"),function(b){e=b.indexOf(":");c=N(aa(b.substr(0,e)));d=aa(b.substr(e+1));c&&(a[c]=a[c]?a[c]+", "+d:d)});return a}function wc(b){var a=T(b)?b:t;return function(c){a||(a=vc(b));return c?a[N(c)]||null:a}}function xc(b,a,c){if(P(c))return c(b,
68 a);r(c,function(c){b=c(b,a)});return b}function Ud(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d={"Content-Type":"application/json;charset=utf-8"},e=this.defaults={transformResponse:[function(d){z(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=cc(d)));return d}],transformRequest:[function(a){return T(a)&&"[object File]"!==ya.call(a)&&"[object Blob]"!==ya.call(a)?ta(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ga(d),put:ga(d),patch:ga(d)},xsrfCookieName:"XSRF-TOKEN",
69 xsrfHeaderName:"X-XSRF-TOKEN"},f=this.interceptors=[],g=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,d,n,p){function q(a){function b(a){var d=B({},a,{data:xc(a.data,a.headers,c.transformResponse)});return 200<=a.status&&300>a.status?d:n.reject(d)}var c={method:"get",transformRequest:e.transformRequest,transformResponse:e.transformResponse},d=function(a){var b=e.headers,c=B({},a.headers),d,f,b=B({},b.common,b[N(a.method)]);
70 a:for(d in b){a=N(d);for(f in c)if(N(f)===a)continue a;c[d]=b[d]}(function(a){var b;r(a,function(c,d){P(c)&&(b=c(),null!=b?a[d]=b:delete a[d])})})(c);return c}(a);B(c,a);c.headers=d;c.method=Ia(c.method);var f=[function(a){d=a.headers;var c=xc(a.data,wc(d),a.transformRequest);D(c)&&r(d,function(a,b){"content-type"===N(b)&&delete d[b]});D(a.withCredentials)&&!D(e.withCredentials)&&(a.withCredentials=e.withCredentials);return s(a,c,d).then(b,b)},t],g=n.when(c);for(r(C,function(a){(a.request||a.requestError)&&
71 f.unshift(a.request,a.requestError);(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var m=f.shift(),g=g.then(a,m)}g.success=function(a){g.then(function(b){a(b.data,b.status,b.headers,c)});return g};g.error=function(a){g.then(null,function(b){a(b.data,b.status,b.headers,c)});return g};return g}function s(c,f,g){function h(a,b,c,e){E&&(200<=a&&300>a?E.put(u,[a,b,vc(c),e]):E.remove(u));p(b,a,c,e);d.$$phase||d.$apply()}function p(a,b,d,e){b=Math.max(b,0);(200<=
72 b&&300>b?C.resolve:C.reject)({data:a,status:b,headers:wc(d),config:c,statusText:e})}function s(){var a=Qa(q.pendingRequests,c);-1!==a&&q.pendingRequests.splice(a,1)}var C=n.defer(),r=C.promise,E,F,u=L(c.url,c.params);q.pendingRequests.push(c);r.then(s,s);!c.cache&&!e.cache||(!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method)||(E=T(c.cache)?c.cache:T(e.cache)?e.cache:v);if(E)if(F=E.get(u),A(F)){if(F&&P(F.then))return F.then(s,s),F;H(F)?p(F[1],F[0],ga(F[2]),F[3]):p(F,200,{},"OK")}else E.put(u,r);D(F)&&
73 ((F=Pb(c.url)?b.cookies()[c.xsrfCookieName||e.xsrfCookieName]:t)&&(g[c.xsrfHeaderName||e.xsrfHeaderName]=F),a(c.method,u,f,h,g,c.timeout,c.withCredentials,c.responseType));return r}function L(a,b){if(!b)return a;var c=[];Tc(b,function(a,b){null===a||D(a)||(H(a)||(a=[a]),r(a,function(a){T(a)&&(sa(a)?a=a.toISOString():T(a)&&(a=ta(a)));c.push(Ba(b)+"="+Ba(a))}))});0<c.length&&(a+=(-1==a.indexOf("?")?"?":"&")+c.join("&"));return a}var v=c("$http"),C=[];r(f,function(a){C.unshift(z(a)?p.get(a):p.invoke(a))});
74 r(g,function(a,b){var c=z(a)?p.get(a):p.invoke(a);C.splice(b,0,{response:function(a){return c(n.when(a))},responseError:function(a){return c(n.reject(a))}})});q.pendingRequests=[];(function(a){r(arguments,function(a){q[a]=function(b,c){return q(B(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){r(arguments,function(a){q[a]=function(b,c,d){return q(B(d||{},{method:a,url:b,data:c}))}})})("post","put");q.defaults=e;return q}]}function ve(b){if(8>=R&&(!b.match(/^(get|post|head|put|delete|options)$/i)||
75 !Q.XMLHttpRequest))return new Q.ActiveXObject("Microsoft.XMLHTTP");if(Q.XMLHttpRequest)return new Q.XMLHttpRequest;throw x("$httpBackend")("noxhr");}function Vd(){this.$get=["$browser","$window","$document",function(b,a,c){return we(b,ve,b.defer,a.angular.callbacks,c[0])}]}function we(b,a,c,d,e){function f(a,b,c){var f=e.createElement("script"),g=null;f.type="text/javascript";f.src=a;f.async=!0;g=function(a){Za(f,"load",g);Za(f,"error",g);e.body.removeChild(f);f=null;var k=-1,s="unknown";a&&("load"!==
76 a.type||d[b].called||(a={type:"error"}),s=a.type,k="error"===a.type?404:200);c&&c(k,s)};rb(f,"load",g);rb(f,"error",g);8>=R&&(f.onreadystatechange=function(){z(f.readyState)&&/loaded|complete/.test(f.readyState)&&(f.onreadystatechange=null,g({type:"load"}))});e.body.appendChild(f);return g}var g=-1;return function(e,m,h,l,n,p,q,s){function L(){C=g;I&&I();w&&w.abort()}function v(a,d,e,f,g){K&&c.cancel(K);I=w=null;0===d&&(d=e?200:"file"==ua(m).protocol?404:0);a(1223===d?204:d,e,f,g||"");b.$$completeOutstandingRequest(y)}
77 var C;b.$$incOutstandingRequestCount();m=m||b.url();if("jsonp"==N(e)){var O="_"+(d.counter++).toString(36);d[O]=function(a){d[O].data=a;d[O].called=!0};var I=f(m.replace("JSON_CALLBACK","angular.callbacks."+O),O,function(a,b){v(l,a,d[O].data,"",b);d[O]=y})}else{var w=a(e);w.open(e,m,!0);r(n,function(a,b){A(a)&&w.setRequestHeader(b,a)});w.onreadystatechange=function(){if(w&&4==w.readyState){var a=null,b=null,c="";C!==g&&(a=w.getAllResponseHeaders(),b="response"in w?w.response:w.responseText);C===g&&
78 10>R||(c=w.statusText);v(l,C||w.status,b,a,c)}};q&&(w.withCredentials=!0);if(s)try{w.responseType=s}catch(da){if("json"!==s)throw da;}w.send(h||null)}if(0<p)var K=c(L,p);else p&&P(p.then)&&p.then(L)}}function Sd(){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,h,l){for(var n,p,q=0,s=[],L=f.length,v=!1,C=[];q<L;)-1!=(n=f.indexOf(b,q))&&-1!=(p=f.indexOf(a,
79 n+g))?(q!=n&&s.push(f.substring(q,n)),s.push(q=c(v=f.substring(n+g,p))),q.exp=v,q=p+k,v=!0):(q!=L&&s.push(f.substring(q)),q=L);(L=s.length)||(s.push(""),L=1);if(l&&1<s.length)throw yc("noconcat",f);if(!h||v)return C.length=L,q=function(a){try{for(var b=0,c=L,g;b<c;b++){if("function"==typeof(g=s[b]))if(g=g(a),g=l?e.getTrusted(l,g):e.valueOf(g),null==g)g="";else switch(typeof g){case "string":break;case "number":g=""+g;break;default:g=ta(g)}C[b]=g}return C.join("")}catch(k){a=yc("interr",f,k.toString()),
80 d(a)}},q.exp=f,q.parts=s,q}var g=b.length,k=a.length;f.startSymbol=function(){return b};f.endSymbol=function(){return a};return f}]}function Td(){this.$get=["$rootScope","$window","$q",function(b,a,c){function d(d,g,k,m){var h=a.setInterval,l=a.clearInterval,n=c.defer(),p=n.promise,q=0,s=A(m)&&!m;k=A(k)?k:0;p.then(null,null,d);p.$$intervalId=h(function(){n.notify(q++);0<k&&q>=k&&(n.resolve(q),l(p.$$intervalId),delete e[p.$$intervalId]);s||b.$apply()},g);e[p.$$intervalId]=n;return p}var e={};d.cancel=
81 function(b){return b&&b.$$intervalId in e?(e[b.$$intervalId].reject("canceled"),a.clearInterval(b.$$intervalId),delete e[b.$$intervalId],!0):!1};return d}]}function bd(){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(" "),
82 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]=
83 jb(b[a]);return b.join("/")}function zc(b,a,c){b=ua(b,c);a.$$protocol=b.protocol;a.$$host=b.hostname;a.$$port=Z(b.port)||xe[b.protocol]||null}function Ac(b,a,c){var d="/"!==b.charAt(0);d&&(b="/"+b);b=ua(b,c);a.$$path=decodeURIComponent(d&&"/"===b.pathname.charAt(0)?b.pathname.substring(1):b.pathname);a.$$search=ec(b.search);a.$$hash=decodeURIComponent(b.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function qa(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function cb(b){var a=
84 b.indexOf("#");return-1==a?b:b.substr(0,a)}function Rb(b){return b.substr(0,cb(b).lastIndexOf("/")+1)}function Bc(b,a){this.$$html5=!0;a=a||"";var c=Rb(b);zc(b,this,b);this.$$parse=function(a){var e=qa(c,a);if(!z(e))throw Sb("ipthprfx",a,c);Ac(e,this,b);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Cb(this.$$search),b=this.$$hash?"#"+jb(this.$$hash):"";this.$$url=Qb(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$rewrite=function(d){var e;
85 if((e=qa(b,d))!==t)return d=e,(e=qa(a,e))!==t?c+(qa("/",e)||e):b+d;if((e=qa(c,d))!==t)return c+e;if(c==d+"/")return c}}function Tb(b,a){var c=Rb(b);zc(b,this,b);this.$$parse=function(d){var e=qa(b,d)||qa(c,d),e="#"==e.charAt(0)?qa(a,e):this.$$html5?e:"";if(!z(e))throw Sb("ihshprfx",d,a);Ac(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=Cb(this.$$search),e=this.$$hash?
86 "#"+jb(this.$$hash):"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$rewrite=function(a){if(cb(b)==cb(a))return a}}function Ub(b,a){this.$$html5=!0;Tb.apply(this,arguments);var c=Rb(b);this.$$rewrite=function(d){var e;if(b==cb(d))return d;if(e=qa(c,d))return b+a+e;if(c===d+"/")return c};this.$$compose=function(){var c=Cb(this.$$search),e=this.$$hash?"#"+jb(this.$$hash):"";this.$$url=Qb(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function sb(b){return function(){return this[b]}}
87 function Cc(b,a){return function(c){if(D(c))return this[b];this[b]=a(c);this.$$compose();return this}}function Wd(){var b="",a=!1;this.hashPrefix=function(a){return A(a)?(b=a,this):b};this.html5Mode=function(b){return A(b)?(a=b,this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,f){function g(a){c.$broadcast("$locationChangeSuccess",k.absUrl(),a)}var k,m,h=d.baseHref(),l=d.url(),n;a?(n=l.substring(0,l.indexOf("/",l.indexOf("//")+2))+(h||"/"),m=e.history?Bc:Ub):(n=
88 cb(l),m=Tb);k=new m(n,"#"+b);k.$$parse(k.$$rewrite(l));var p=/^\s*(javascript|mailto):/i;f.on("click",function(a){if(!a.ctrlKey&&!a.metaKey&&2!=a.which){for(var e=u(a.target);"a"!==N(e[0].nodeName);)if(e[0]===f[0]||!(e=e.parent())[0])return;var g=e.prop("href");T(g)&&"[object SVGAnimatedString]"===g.toString()&&(g=ua(g.animVal).href);if(!p.test(g)){if(m===Ub){var h=e.attr("href")||e.attr("xlink:href");if(h&&0>h.indexOf("://"))if(g="#"+b,"/"==h[0])g=n+g+h;else if("#"==h[0])g=n+g+(k.path()||"/")+h;
89 else{var l=k.path().split("/"),h=h.split("/");2!==l.length||l[1]||(l.length=1);for(var q=0;q<h.length;q++)"."!=h[q]&&(".."==h[q]?l.pop():h[q].length&&l.push(h[q]));g=n+g+l.join("/")}}l=k.$$rewrite(g);g&&(!e.attr("target")&&l&&!a.isDefaultPrevented())&&(a.preventDefault(),l!=d.url()&&(k.$$parse(l),c.$apply(),Q.angular["ff-684208-preventDefault"]=!0))}}});k.absUrl()!=l&&d.url(k.absUrl(),!0);d.onUrlChange(function(a){k.absUrl()!=a&&(c.$evalAsync(function(){var b=k.absUrl();k.$$parse(a);c.$broadcast("$locationChangeStart",
90 a,b).defaultPrevented?(k.$$parse(b),d.url(b)):g(b)}),c.$$phase||c.$digest())});var q=0;c.$watch(function(){var a=d.url(),b=k.$$replace;q&&a==k.absUrl()||(q++,c.$evalAsync(function(){c.$broadcast("$locationChangeStart",k.absUrl(),a).defaultPrevented?k.$$parse(a):(d.url(k.absUrl(),b),g(a))}));k.$$replace=!1;return q});return k}]}function Xd(){var b=!0,a=this;this.debugEnabled=function(a){return A(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&
91 -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||y;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 ja(b,
92 a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw ka("isecfld",a);return b}function Oa(b,a){if(b){if(b.constructor===b)throw ka("isecfn",a);if(b.document&&b.location&&b.alert&&b.setInterval)throw ka("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw ka("isecdom",a);if(b===Object)throw ka("isecobj",a);}return b}function tb(b,a,c,d,e){e=e||{};a=a.split(".");for(var f,g=0;1<a.length;g++){f=ja(a.shift(),d);
93 var k=b[f];k||(k={},b[f]=k);b=k;b.then&&e.unwrapPromises&&(va(d),"$$v"in b||function(a){a.then(function(b){a.$$v=b})}(b),b.$$v===t&&(b.$$v={}),b=b.$$v)}f=ja(a.shift(),d);Oa(b,d);Oa(b[f],d);return b[f]=c}function Dc(b,a,c,d,e,f,g){ja(b,f);ja(a,f);ja(c,f);ja(d,f);ja(e,f);return g.unwrapPromises?function(g,m){var h=m&&m.hasOwnProperty(b)?m:g,l;if(null==h)return h;(h=h[b])&&h.then&&(va(f),"$$v"in h||(l=h,l.$$v=t,l.then(function(a){l.$$v=a})),h=h.$$v);if(!a)return h;if(null==h)return t;(h=h[a])&&h.then&&
94 (va(f),"$$v"in h||(l=h,l.$$v=t,l.then(function(a){l.$$v=a})),h=h.$$v);if(!c)return h;if(null==h)return t;(h=h[c])&&h.then&&(va(f),"$$v"in h||(l=h,l.$$v=t,l.then(function(a){l.$$v=a})),h=h.$$v);if(!d)return h;if(null==h)return t;(h=h[d])&&h.then&&(va(f),"$$v"in h||(l=h,l.$$v=t,l.then(function(a){l.$$v=a})),h=h.$$v);if(!e)return h;if(null==h)return t;(h=h[e])&&h.then&&(va(f),"$$v"in h||(l=h,l.$$v=t,l.then(function(a){l.$$v=a})),h=h.$$v);return h}:function(f,g){var h=g&&g.hasOwnProperty(b)?g:f;if(null==
95 h)return h;h=h[b];if(!a)return h;if(null==h)return t;h=h[a];if(!c)return h;if(null==h)return t;h=h[c];if(!d)return h;if(null==h)return t;h=h[d];return e?null==h?t:h=h[e]:h}}function Ec(b,a,c){if(Vb.hasOwnProperty(b))return Vb[b];var d=b.split("."),e=d.length,f;if(a.csp)f=6>e?Dc(d[0],d[1],d[2],d[3],d[4],c,a):function(b,f){var g=0,k;do k=Dc(d[g++],d[g++],d[g++],d[g++],d[g++],c,a)(b,f),f=t,b=k;while(g<e);return k};else{var g="var p;\n";r(d,function(b,d){ja(b,c);g+="if(s == null) return undefined;\ns="+
96 (d?"s":'((k&&k.hasOwnProperty("'+b+'"))?k:s)')+'["'+b+'"];\n'+(a.unwrapPromises?'if (s && s.then) {\n pw("'+c.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':"")});var g=g+"return s;",k=new Function("s","k","pw",g);k.toString=$(g);f=a.unwrapPromises?function(a,b){return k(a,b,va)}:k}"hasOwnProperty"!==b&&(Vb[b]=f);return f}function Yd(){var b={},a={csp:!1,unwrapPromises:!1,logPromiseWarnings:!0};this.unwrapPromises=
97 function(b){return A(b)?(a.unwrapPromises=!!b,this):a.unwrapPromises};this.logPromiseWarnings=function(b){return A(b)?(a.logPromiseWarnings=b,this):a.logPromiseWarnings};this.$get=["$filter","$sniffer","$log",function(c,d,e){a.csp=d.csp;va=function(b){a.logPromiseWarnings&&!Fc.hasOwnProperty(b)&&(Fc[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];
98 e=new Wb(a);e=(new db(e,c,a)).parse(d);"hasOwnProperty"!==d&&(b[d]=e);return e;case "function":return d;default:return y}}}]}function $d(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return ye(function(a){b.$evalAsync(a)},a)}]}function ye(b,a){function c(a){return a}function d(a){return g(a)}var e=function(){var g=[],h,l;return l={resolve:function(a){if(g){var c=g;g=t;h=f(a);c.length&&b(function(){for(var a,b=0,d=c.length;b<d;b++)a=c[b],h.then(a[0],a[1],a[2])})}},reject:function(a){l.resolve(k(a))},
99 notify:function(a){if(g){var c=g;g.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,k){var l=e(),L=function(d){try{l.resolve((P(b)?b:c)(d))}catch(e){l.reject(e),a(e)}},v=function(b){try{l.resolve((P(f)?f:d)(b))}catch(c){l.reject(c),a(c)}},C=function(b){try{l.notify((P(k)?k:c)(b))}catch(d){a(d)}};g?g.push([L,v,C]):h.then(L,v,C);return l.promise},"catch":function(a){return this.then(null,a)},"finally":function(a){function b(a,c){var d=e();c?d.resolve(a):
100 d.reject(a);return d.promise}function d(e,f){var g=null;try{g=(a||c)()}catch(k){return b(k,!1)}return g&&P(g.then)?g.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&&P(a.then)?a:{then:function(c){var d=e();b(function(){d.resolve(c(a))});return d.promise}}},g=function(a){var b=e();b.reject(a);return b.promise},k=function(c){return{then:function(f,g){var k=e();b(function(){try{k.resolve((P(g)?
101 g:d)(c))}catch(b){k.reject(b),a(b)}});return k.promise}}};return{defer:e,reject:g,when:function(k,h,l,n){var p=e(),q,s=function(b){try{return(P(h)?h:c)(b)}catch(d){return a(d),g(d)}},L=function(b){try{return(P(l)?l:d)(b)}catch(c){return a(c),g(c)}},v=function(b){try{return(P(n)?n:c)(b)}catch(d){a(d)}};b(function(){f(k).then(function(a){q||(q=!0,p.resolve(f(a).then(s,L,v)))},function(a){q||(q=!0,p.resolve(L(a)))},function(a){q||p.notify(v(a))})});return p.promise},all:function(a){var b=e(),c=0,d=H(a)?
102 []:{};r(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 ge(){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)}}:
103 function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}};f.supported=e;return f}]}function Zd(){var b=10,a=x("$rootScope"),c=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(d,e,f,g){function k(){this.$id=gb();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=
104 [];this.$$listeners={};this.$$listenerCount={};this.$$isolateBindings={}}function m(b){if(p.$$phase)throw a("inprog",p.$$phase);p.$$phase=b}function h(a,b){var c=f(a);Va(c,b);return c}function l(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function n(){}k.prototype={constructor:k,$new:function(a){a?(a=new k,a.$root=this.$root,a.$$asyncQueue=this.$$asyncQueue,a.$$postDigestQueue=this.$$postDigestQueue):(this.$$childScopeClass||(this.$$childScopeClass=
105 function(){this.$$watchers=this.$$nextSibling=this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$id=gb();this.$$childScopeClass=null},this.$$childScopeClass.prototype=this),a=new this.$$childScopeClass);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=h(a,"watch"),f=this.$$watchers,g={fn:b,last:n,get:e,exp:a,
106 eq:!!d};c=null;if(!P(b)){var k=h(b||y,"listener");g.fn=function(a,b,c){k(c)}}if("string"==typeof a&&e.constant){var m=g.fn;g.fn=function(a,b,c){m.call(this,a,b,c);Ra(f,g)}}f||(f=this.$$watchers=[]);f.unshift(g);return function(){Ra(f,g);c=null}},$watchCollection:function(a,b){var c=this,d,e,g,k=1<b.length,h=0,m=f(a),l=[],p={},n=!0,r=0;return this.$watch(function(){d=m(c);var a,b,f;if(T(d))if(fb(d))for(e!==l&&(e=l,r=e.length=0,h++),a=d.length,r!==a&&(h++,e.length=r=a),b=0;b<a;b++)f=e[b]!==e[b]&&d[b]!==
107 d[b],f||e[b]===d[b]||(h++,e[b]=d[b]);else{e!==p&&(e=p={},r=0,h++);a=0;for(b in d)d.hasOwnProperty(b)&&(a++,e.hasOwnProperty(b)?(f=e[b]!==e[b]&&d[b]!==d[b],f||e[b]===d[b]||(h++,e[b]=d[b])):(r++,e[b]=d[b],h++));if(r>a)for(b in h++,e)e.hasOwnProperty(b)&&!d.hasOwnProperty(b)&&(r--,delete e[b])}else e!==d&&(e=d,h++);return h},function(){n?(n=!1,b(d,d,c)):b(d,g,c);if(k)if(T(d))if(fb(d)){g=Array(d.length);for(var a=0;a<d.length;a++)g[a]=d[a]}else for(a in g={},d)ib.call(d,a)&&(g[a]=d[a]);else g=d})},$digest:function(){var d,
108 f,g,k,h=this.$$asyncQueue,l=this.$$postDigestQueue,r,w,t=b,K,W=[],u,J,E;m("$digest");c=null;do{w=!1;for(K=this;h.length;){try{E=h.shift(),E.scope.$eval(E.expression)}catch(F){p.$$phase=null,e(F)}c=null}a:do{if(k=K.$$watchers)for(r=k.length;r--;)try{if(d=k[r])if((f=d.get(K))!==(g=d.last)&&!(d.eq?za(f,g):"number"===typeof f&&"number"===typeof g&&isNaN(f)&&isNaN(g)))w=!0,c=d,d.last=d.eq?Ha(f,null):f,d.fn(f,g===n?f:g,K),5>t&&(u=4-t,W[u]||(W[u]=[]),J=P(d.exp)?"fn: "+(d.exp.name||d.exp.toString()):d.exp,
109 J+="; newVal: "+ta(f)+"; oldVal: "+ta(g),W[u].push(J));else if(d===c){w=!1;break a}}catch(A){p.$$phase=null,e(A)}if(!(k=K.$$childHead||K!==this&&K.$$nextSibling))for(;K!==this&&!(k=K.$$nextSibling);)K=K.$parent}while(K=k);if((w||h.length)&&!t--)throw p.$$phase=null,a("infdig",b,ta(W));}while(w||h.length);for(p.$$phase=null;l.length;)try{l.shift()()}catch(x){e(x)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this!==p&&(r(this.$$listenerCount,
110 Bb(null,l,this)),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=y,this.$on=
111 this.$watch=function(){return y})}},$eval:function(a,b){return f(a)(this,b)},$evalAsync:function(a){p.$$phase||p.$$asyncQueue.length||g.defer(function(){p.$$asyncQueue.length&&p.$digest()});this.$$asyncQueue.push({scope:this,expression:a})},$$postDigest:function(a){this.$$postDigestQueue.push(a)},$apply:function(a){try{return m("$apply"),this.$eval(a)}catch(b){e(b)}finally{p.$$phase=null;try{p.$digest()}catch(c){throw e(c),c;}}},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=
112 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[Qa(c,b)]=null;l(e,1,a)}},$emit:function(a,b){var c=[],d,f=this,g=!1,k={name:a,targetScope:f,stopPropagation:function(){g=!0},preventDefault:function(){k.defaultPrevented=!0},defaultPrevented:!1},h=[k].concat(Aa.call(arguments,1)),m,l;do{d=f.$$listeners[a]||c;k.currentScope=f;m=0;for(l=d.length;m<l;m++)if(d[m])try{d[m].apply(null,h)}catch(p){e(p)}else d.splice(m,
113 1),m--,l--;if(g)break;f=f.$parent}while(f);return k},$broadcast:function(a,b){for(var c=this,d=this,f={name:a,targetScope:this,preventDefault:function(){f.defaultPrevented=!0},defaultPrevented:!1},g=[f].concat(Aa.call(arguments,1)),k,h;c=d;){f.currentScope=c;d=c.$$listeners[a]||[];k=0;for(h=d.length;k<h;k++)if(d[k])try{d[k].apply(null,g)}catch(m){e(m)}else d.splice(k,1),k--,h--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}return f}};
114 var p=new k;return p}]}function cd(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*((https?|ftp|file):|data:image\/)/;this.aHrefSanitizationWhitelist=function(a){return A(a)?(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return A(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,f;if(!R||8<=R)if(f=ua(c).href,""!==f&&!f.match(e))return"unsafe:"+f;return c}}}function ze(b){if("self"===b)return b;if(z(b)){if(-1<b.indexOf("***"))throw wa("iwcard",b);b=b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,
115 "\\$1").replace(/\x08/g,"\\x08").replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return RegExp("^"+b+"$")}if(hb(b))return RegExp("^"+b.source+"$");throw wa("imatcher");}function Gc(b){var a=[];A(b)&&r(b,function(b){a.push(ze(b))});return a}function be(){this.SCE_CONTEXTS=fa;var b=["self"],a=[];this.resourceUrlWhitelist=function(a){arguments.length&&(b=Gc(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=Gc(b));return a};this.$get=["$injector",function(c){function d(a){var b=
116 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 e=function(a){throw wa("unsafe");};c.has("$sanitize")&&(e=c.get("$sanitize"));var f=d(),g={};g[fa.HTML]=d(f);g[fa.CSS]=d(f);g[fa.URL]=d(f);g[fa.JS]=d(f);g[fa.RESOURCE_URL]=d(g[fa.URL]);return{trustAs:function(a,b){var c=g.hasOwnProperty(a)?g[a]:null;if(!c)throw wa("icontext",
117 a,b);if(null===b||b===t||""===b)return b;if("string"!==typeof b)throw wa("itype",a);return new c(b)},getTrusted:function(c,d){if(null===d||d===t||""===d)return d;var f=g.hasOwnProperty(c)?g[c]:null;if(f&&d instanceof f)return d.$$unwrapTrustedValue();if(c===fa.RESOURCE_URL){var f=ua(d.toString()),l,n,p=!1;l=0;for(n=b.length;l<n;l++)if("self"===b[l]?Pb(f):b[l].exec(f.href)){p=!0;break}if(p)for(l=0,n=a.length;l<n;l++)if("self"===a[l]?Pb(f):a[l].exec(f.href)){p=!1;break}if(p)return d;throw wa("insecurl",
118 d.toString());}if(c===fa.HTML)return e(d);throw wa("unsafe");},valueOf:function(a){return a instanceof f?a.$$unwrapTrustedValue():a}}}]}function ae(){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 wa("iequirks");var e=ga(fa);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},
119 e.valueOf=Ga);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,k=e.trustAs;r(fa,function(a,b){var c=N(b);e[Ya("parse_as_"+c)]=function(b){return f(a,b)};e[Ya("get_trusted_"+c)]=function(b){return g(a,b)};e[Ya("trust_as_"+c)]=function(b){return k(a,b)}});return e}]}function ce(){this.$get=["$window","$document",function(b,a){var c={},d=Z((/android (\d+)/.exec(N((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||
120 {}).userAgent),f=a[0]||{},g=f.documentMode,k,m=/^(Moz|webkit|O|ms)(?=[A-Z])/,h=f.body&&f.body.style,l=!1,n=!1;if(h){for(var p in h)if(l=m.exec(p)){k=l[0];k=k.substr(0,1).toUpperCase()+k.substr(1);break}k||(k="WebkitOpacity"in h&&"webkit");l=!!("transition"in h||k+"Transition"in h);n=!!("animation"in h||k+"Animation"in h);!d||l&&n||(l=z(f.body.style.webkitTransition),n=z(f.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hashchange:"onhashchange"in b&&(!g||7<
121 g),hasEvent:function(a){if("input"==a&&9==R)return!1;if(D(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:Wa(),vendorPrefix:k,transitions:l,animations:n,android:d,msie:R,msieDocumentMode:g}}]}function ee(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,k,m){var h=c.defer(),l=h.promise,n=A(m)&&!m;k=a.defer(function(){try{h.resolve(e())}catch(a){h.reject(a),d(a)}finally{delete f[l.$$timeoutId]}n||b.$apply()},k);l.$$timeoutId=k;f[k]=h;
122 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 ua(b,a){var c=b;R&&(V.setAttribute("href",c),c=V.href);V.setAttribute("href",c);return{href:V.href,protocol:V.protocol?V.protocol.replace(/:$/,""):"",host:V.host,search:V.search?V.search.replace(/^\?/,""):"",hash:V.hash?V.hash.replace(/^#/,""):"",hostname:V.hostname,port:V.port,pathname:"/"===V.pathname.charAt(0)?V.pathname:
123 "/"+V.pathname}}function Pb(b){b=z(b)?ua(b):b;return b.protocol===Hc.protocol&&b.host===Hc.host}function fe(){this.$get=$(Q)}function mc(b){function a(d,e){if(T(d)){var f={};r(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",Ic);a("date",Jc);a("filter",Ae);a("json",Be);a("limitTo",Ce);a("lowercase",De);a("number",Kc);a("orderBy",Lc);a("uppercase",Ee)}function Ae(){return function(b,
124 a,c){if(!H(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 Ua.equals(a,b)}:function(a,b){if(a&&b&&"object"===typeof a&&"object"===typeof b){for(var d in a)if("$"!==d.charAt(0)&&ib.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,
125 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 k=
126 b[g];e.check(k)&&d.push(k)}return d}}function Ic(b){var a=b.NUMBER_FORMATS;return function(b,d){D(d)&&(d=a.CURRENCY_SYM);return Mc(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,2).replace(/\u00A4/g,d)}}function Kc(b){var a=b.NUMBER_FORMATS;return function(b,d){return Mc(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}function Mc(b,a,c,d,e){if(null==b||!isFinite(b)||T(b))return"";var f=0>b;b=Math.abs(b);var g=b+"",k="",m=[],h=!1;if(-1!==g.indexOf("e")){var l=g.match(/([\d\.]+)e(-?)(\d+)/);l&&"-"==l[2]&&
127 l[3]>e+1?(g="0",b=0):(k=g,h=!0)}if(h)0<e&&(-1<b&&1>b)&&(k=b.toFixed(e));else{g=(g.split(Nc)[1]||"").length;D(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);b=(""+b).split(Nc);g=b[0];b=b[1]||"";var l=0,n=a.lgSize,p=a.gSize;if(g.length>=n+p)for(l=g.length-n,h=0;h<l;h++)0===(l-h)%p&&0!==h&&(k+=c),k+=g.charAt(h);for(h=l;h<g.length;h++)0===(g.length-h)%n&&0!==h&&(k+=c),k+=g.charAt(h);for(;b.length<e;)b+="0";e&&"0"!==e&&(k+=d+b.substr(0,e))}m.push(f?
128 a.negPre:a.posPre);m.push(k);m.push(f?a.negSuf:a.posSuf);return m.join("")}function Xb(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 Y(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 Xb(e,a,d)}}function ub(b,a){return function(c,d){var e=c["get"+b](),f=Ia(a?"SHORT"+b:b);return d[f][e]}}function Jc(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var f=0,g=0,k=b[8]?a.setUTCFullYear:
129 a.setFullYear,m=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Z(b[9]+b[10]),g=Z(b[9]+b[11]));k.call(a,Z(b[1]),Z(b[2])-1,Z(b[3]));f=Z(b[4]||0)-f;g=Z(b[5]||0)-g;k=Z(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));m.call(a,f,g,k,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=[],k,m;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;z(c)&&(c=Fe.test(c)?Z(c):a(c));Ab(c)&&(c=new Date(c));if(!sa(c))return c;
130 for(;e;)(m=Ge.exec(e))?(g=g.concat(Aa.call(m,1)),e=g.pop()):(g.push(e),e=null);r(g,function(a){k=He[a];f+=k?k(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return f}}function Be(){return function(b){return ta(b,!0)}}function Ce(){return function(b,a){if(!H(b)&&!z(b))return b;a=Infinity===Math.abs(Number(a))?Number(a):Z(a);if(z(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);
131 for(;d<e;d++)c.push(b[d]);return c}}function Lc(b){return function(a,c,d){function e(a,b){return Ta(b)?function(b,c){return a(c,b)}:a}function f(a,b){var c=typeof a,d=typeof b;return c==d?(sa(a)&&sa(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(!H(a)||!c)return a;c=H(c)?c:[c];c=Vc(c,function(a){var c=!1,d=a||Ga;if(z(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))c="-"==a.charAt(0),a=a.substring(1);d=b(a);if(d.constant){var g=d();return e(function(a,
132 b){return f(a[g],b[g])},c)}}return e(function(a,b){return f(d(a),d(b))},c)});for(var g=[],k=0;k<a.length;k++)g.push(a[k]);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 xa(b){P(b)&&(b={link:b});b.restrict=b.restrict||"AC";return $(b)}function Oc(b,a,c,d){function e(a,c){c=c?"-"+kb(c,"-"):"";d.removeClass(b,(a?vb:wb)+c);d.addClass(b,(a?wb:vb)+c)}var f=this,g=b.parent().controller("form")||xb,k=0,m=f.$error={},h=[];f.$name=a.name||
133 a.ngForm;f.$dirty=!1;f.$pristine=!0;f.$valid=!0;f.$invalid=!1;g.$addControl(f);b.addClass(Pa);e(!0);f.$addControl=function(a){Ca(a.$name,"input");h.push(a);a.$name&&(f[a.$name]=a)};f.$removeControl=function(a){a.$name&&f[a.$name]===a&&delete f[a.$name];r(m,function(b,c){f.$setValidity(c,!0,a)});Ra(h,a)};f.$setValidity=function(a,b,c){var d=m[a];if(b)d&&(Ra(d,c),d.length||(k--,k||(e(b),f.$valid=!0,f.$invalid=!1),m[a]=!1,e(!0,a),g.$setValidity(a,!0,f)));else{k||e(b);if(d){if(-1!=Qa(d,c))return}else m[a]=
134 d=[],k++,e(!1,a),g.$setValidity(a,!1,f);d.push(c);f.$valid=!1;f.$invalid=!0}};f.$setDirty=function(){d.removeClass(b,Pa);d.addClass(b,yb);f.$dirty=!0;f.$pristine=!1;g.$setDirty()};f.$setPristine=function(){d.removeClass(b,yb);d.addClass(b,Pa);f.$dirty=!1;f.$pristine=!0;r(h,function(a){a.$setPristine()})}}function ra(b,a,c,d){b.$setValidity(a,c);return c?d:t}function Pc(b,a){var c,d;if(a)for(c=0;c<a.length;++c)if(d=a[c],b[d])return!0;return!1}function Ie(b,a,c,d,e){T(e)&&(b.$$hasNativeValidators=!0,
135 b.$parsers.push(function(f){if(b.$error[a]||Pc(e,d)||!Pc(e,c))return f;b.$setValidity(a,!1)}))}function zb(b,a,c,d,e,f){var g=a.prop(Je),k=a[0].placeholder,m={},h=N(a[0].type);d.$$validityState=g;if(!e.android){var l=!1;a.on("compositionstart",function(a){l=!0});a.on("compositionend",function(){l=!1;n()})}var n=function(e){if(!l){var f=a.val();if(R&&"input"===(e||m).type&&a[0].placeholder!==k)k=a[0].placeholder;else if("password"!==h&&Ta(c.ngTrim||"T")&&(f=aa(f)),e=g&&d.$$hasNativeValidators,d.$viewValue!==
136 f||""===f&&e)b.$$phase?d.$setViewValue(f):b.$apply(function(){d.$setViewValue(f)})}};if(e.hasEvent("input"))a.on("input",n);else{var p,q=function(){p||(p=f.defer(function(){n();p=null}))};a.on("keydown",function(a){a=a.keyCode;91===a||(15<a&&19>a||37<=a&&40>=a)||q()});if(e.hasEvent("paste"))a.on("paste cut",q)}a.on("change",n);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)};var s=c.ngPattern;s&&((e=s.match(/^\/(.*)\/([gim]*)$/))?(s=RegExp(e[1],e[2]),e=function(a){return ra(d,
137 "pattern",d.$isEmpty(a)||s.test(a),a)}):e=function(c){var e=b.$eval(s);if(!e||!e.test)throw x("ngPattern")("noregexp",s,e,ha(a));return ra(d,"pattern",d.$isEmpty(c)||e.test(c),c)},d.$formatters.push(e),d.$parsers.push(e));if(c.ngMinlength){var r=Z(c.ngMinlength);e=function(a){return ra(d,"minlength",d.$isEmpty(a)||a.length>=r,a)};d.$parsers.push(e);d.$formatters.push(e)}if(c.ngMaxlength){var v=Z(c.ngMaxlength);e=function(a){return ra(d,"maxlength",d.$isEmpty(a)||a.length<=v,a)};d.$parsers.push(e);
138 d.$formatters.push(e)}}function Yb(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],l=0;l<b.length;l++)if(e==b[l])continue a;c.push(e)}return c}function e(a){if(!H(a)){if(z(a))return a.split(" ");if(T(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,k){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<
139 b)&&d.push(a)});g.data("$classCounts",c);return d.join(" ")}function h(b){if(!0===a||f.$index%2===a){var h=e(b||[]);if(!l){var q=m(h,1);k.$addClass(q)}else if(!za(b,l)){var s=e(l),q=d(h,s),h=d(s,h),h=m(h,-1),q=m(q,1);0===q.length?c.removeClass(g,h):0===h.length?c.addClass(g,q):c.setClass(g,q,h)}}l=ga(b)}var l;f.$watch(k[b],h,!0);k.$observe("class",function(a){h(f.$eval(k[b]))});"ngClass"!==b&&f.$watch("$index",function(c,d){var g=c&1;if(g!==(d&1)){var h=e(f.$eval(k[b]));g===a?(g=m(h,1),k.$addClass(g)):
140 (g=m(h,-1),k.$removeClass(g))}})}}}]}var Je="validity",N=function(b){return z(b)?b.toLowerCase():b},ib=Object.prototype.hasOwnProperty,Ia=function(b){return z(b)?b.toUpperCase():b},R,u,Da,Aa=[].slice,Ke=[].push,ya=Object.prototype.toString,Sa=x("ng"),Ua=Q.angular||(Q.angular={}),Xa,Ma,la=["0","0","0"];R=Z((/msie (\d+)/.exec(N(navigator.userAgent))||[])[1]);isNaN(R)&&(R=Z((/trident\/.*; rv:(\d+)/.exec(N(navigator.userAgent))||[])[1]));y.$inject=[];Ga.$inject=[];var H=function(){return P(Array.isArray)?
141 Array.isArray:function(b){return"[object Array]"===ya.call(b)}}(),aa=function(){return String.prototype.trim?function(b){return z(b)?b.trim():b}:function(b){return z(b)?b.replace(/^\s\s*/,"").replace(/\s\s*$/,""):b}}();Ma=9>R?function(b){b=b.nodeName?b:b[0];return b.scopeName&&"HTML"!=b.scopeName?Ia(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var Wa=function(){if(A(Wa.isActive_))return Wa.isActive_;var b=!(!X.querySelector("[ng-csp]")&&!X.querySelector("[data-ng-csp]"));
142 if(!b)try{new Function("")}catch(a){b=!0}return Wa.isActive_=b},Yc=/[A-Z]/g,ad={full:"1.2.23",major:1,minor:2,dot:23,codeName:"superficial-malady"};S.expando="ng339";var $a=S.cache={},ne=1,rb=Q.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},Za=Q.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)};S._data=function(b){return this.cache[b[this.expando]]||{}};var ie=/([\:\-\_]+(.))/g,
143 je=/^moz([A-Z])/,Hb=x("jqLite"),ke=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Ib=/<|&#?\w+;/,le=/<([\w:]+)/,me=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ba={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,"",""]};ba.optgroup=ba.option;ba.tbody=ba.tfoot=ba.colgroup=ba.caption=ba.thead;ba.th=
144 ba.td;var La=S.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===X.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),S(Q).on("load",a))},toString:function(){var b=[];r(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?u(this[b]):u(this[this.length+b])},length:0,push:Ke,sort:[].sort,splice:[].splice},ob={};r("multiple selected checked disabled readOnly required open".split(" "),function(b){ob[N(b)]=b});var rc={};r("input select option textarea button form details".split(" "),
145 function(b){rc[Ia(b)]=!0});r({data:Mb,removeData:Lb},function(b,a){S[a]=b});r({data:Mb,inheritedData:nb,scope:function(b){return u.data(b,"$scope")||nb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return u.data(b,"$isolateScope")||u.data(b,"$isolateScopeNoTemplate")},controller:oc,injector:function(b){return nb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Nb,css:function(b,a,c){a=Ya(a);if(A(c))b.style[a]=c;else{var d;8>=R&&(d=b.currentStyle&&b.currentStyle[a],
146 ""===d&&(d="auto"));d=d||b.style[a];8>=R&&(d=""===d?t:d);return d}},attr:function(b,a,c){var d=N(a);if(ob[d])if(A(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||y).specified?d:t;else if(A(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?t:b},prop:function(b,a,c){if(A(c))b[a]=c;else return b[a]},text:function(){function b(b,d){var e=a[b.nodeType];if(D(d))return e?b[e]:"";b[e]=d}var a=[];9>R?(a[1]=
147 "innerText",a[3]="nodeValue"):a[1]=a[3]="textContent";b.$dv="";return b}(),val:function(b,a){if(D(a)){if("SELECT"===Ma(b)&&b.multiple){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(D(a))return b.innerHTML;for(var c=0,d=b.childNodes;c<d.length;c++)Ja(d[c]);b.innerHTML=a},empty:pc},function(b,a){S.prototype[a]=function(a,d){var e,f,g=this.length;if(b!==pc&&(2==b.length&&b!==Nb&&b!==oc?a:d)===t){if(T(a)){for(e=
148 0;e<g;e++)if(b===Mb)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv;g=e===t?Math.min(g,1):g;for(f=0;f<g;f++){var k=b(this[f],a,d);e=e?e+k:k}return e}for(e=0;e<g;e++)b(this[e],a,d);return this}});r({removeData:Lb,dealoc:Ja,on:function a(c,d,e,f){if(A(f))throw Hb("onargs");var g=ma(c,"events"),k=ma(c,"handle");g||ma(c,"events",g={});k||ma(c,"handle",k=oe(c,g));r(d.split(" "),function(d){var f=g[d];if(!f){if("mouseenter"==d||"mouseleave"==d){var l=X.body.contains||X.body.compareDocumentPosition?
149 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))||k(a,d)})}else rb(c,d,k),g[d]=[];f=g[d]}f.push(e)})},off:nc,one:function(a,c,d){a=u(a);a.on(c,function f(){a.off(c,
150 d);a.off(c,f)});a.on(c,d)},replaceWith:function(a,c){var d,e=a.parentNode;Ja(a);r(new S(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){r(new S(c),function(c){1!==a.nodeType&&11!==a.nodeType||a.appendChild(c)})},prepend:function(a,c){if(1===a.nodeType){var d=a.firstChild;r(new S(c),function(c){a.insertBefore(c,
151 d)})}},wrap:function(a,c){c=u(c)[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:function(a){Ja(a);var c=a.parentNode;c&&c.removeChild(a)},after:function(a,c){var d=a,e=a.parentNode;r(new S(c),function(a){e.insertBefore(a,d.nextSibling);d=a})},addClass:mb,removeClass:lb,toggleClass:function(a,c,d){c&&r(c.split(" "),function(c){var f=d;D(f)&&(f=!Nb(a,c));(f?mb:lb)(a,c)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){if(a.nextElementSibling)return a.nextElementSibling;
152 for(a=a.nextSibling;null!=a&&1!==a.nodeType;)a=a.nextSibling;return a},find:function(a,c){return a.getElementsByTagName?a.getElementsByTagName(c):[]},clone:Kb,triggerHandler:function(a,c,d){var e,f;e=c.type||c;var g=(ma(a,"events")||{})[e];g&&(e={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===this.defaultPrevented},stopPropagation:y,type:e,target:a},c.type&&(e=B(e,c)),c=ga(g),f=d?[e].concat(d):[e],r(c,function(c){c.apply(a,f)}))}},function(a,c){S.prototype[c]=
153 function(c,e,f){for(var g,k=0;k<this.length;k++)D(g)?(g=a(this[k],c,e,f),A(g)&&(g=u(g))):Jb(g,a(this[k],c,e,f));return A(g)?g:this};S.prototype.bind=S.prototype.on;S.prototype.unbind=S.prototype.off});ab.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 qe=/^function\s*[^\(]*\(\s*([^\)]*)\)/m,re=/,/,se=/^\s*(_?)(\S+?)\1\s*$/,pe=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
154 bb=x("$injector"),Le=x("$animate"),Md=["$provide",function(a){this.$$selectors={};this.register=function(c,d){var e=c+"-animation";if(c&&"."!=c.charAt(0))throw Le("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=["$timeout","$$asyncCallback",function(a,d){return{enter:function(a,c,g,k){g?g.after(a):(c&&c[0]||(c=g.parent()),c.append(a));k&&
155 d(k)},leave:function(a,c){a.remove();c&&d(c)},move:function(a,c,d,k){this.enter(a,c,d,k)},addClass:function(a,c,g){c=z(c)?c:H(c)?c.join(" "):"";r(a,function(a){mb(a,c)});g&&d(g)},removeClass:function(a,c,g){c=z(c)?c:H(c)?c.join(" "):"";r(a,function(a){lb(a,c)});g&&d(g)},setClass:function(a,c,g,k){r(a,function(a){mb(a,c);lb(a,g)});k&&d(k)},enabled:y}}]}],ia=x("$compile");ic.$inject=["$provide","$$sanitizeUriProvider"];var ue=/^(x[\:\-_]|data[\:\-_])/i,yc=x("$interpolate"),Me=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,
156 xe={http:80,https:443,ftp:21},Sb=x("$location");Ub.prototype=Tb.prototype=Bc.prototype={$$html5:!1,$$replace:!1,absUrl:sb("$$absUrl"),url:function(a,c){if(D(a))return this.$$url;var d=Me.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:sb("$$protocol"),host:sb("$$host"),port:sb("$$port"),path:Cc("$$path",function(a){return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;
157 case 1:if(z(a))this.$$search=ec(a);else if(T(a))r(a,function(c,e){null==c&&delete a[e]}),this.$$search=a;else throw Sb("isrcharg");break;default:D(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:Cc("$$hash",Ga),replace:function(){this.$$replace=!0;return this}};var ka=x("$parse"),Fc={},va,Ne=Function.prototype.call,Oe=Function.prototype.apply,Qc=Function.prototype.bind,eb={"null":function(){return null},"true":function(){return!0},"false":function(){return!1},
158 undefined:y,"+":function(a,c,d,e){d=d(a,c);e=e(a,c);return A(d)?A(e)?d+e:d:A(e)?e:t},"-":function(a,c,d,e){d=d(a,c);e=e(a,c);return(A(d)?d:0)-(A(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)},"=":y,"===":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,
159 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)}},Pe={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Wb=function(a){this.options=a};Wb.prototype=
160 {constructor:Wb,lex:function(a){this.text=a;this.index=0;this.ch=t;this.lastCh=":";for(this.tokens=[];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();else if(this.is("(){}[].,;:?"))this.tokens.push({index:this.index,text:this.ch}),this.index++;else if(this.isWhitespace(this.ch)){this.index++;continue}else{a=
161 this.ch+this.peek();var c=a+this.peek(2),d=eb[this.ch],e=eb[a],f=eb[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)}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=
162 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=A(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw ka("lexerr",a,c,this.text);
163 },readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=N(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,literal:!0,constant:!0,fn:function(){return a}})},
164 readIdent:function(){for(var a=this,c="",d=this.index,e,f,g,k;this.index<this.text.length;){k=this.text.charAt(this.index);if("."===k||this.isIdent(k)||this.isNumber(k))"."===k&&(e=this.index),c+=k;else break;this.index++}if(e)for(f=this.index;f<this.text.length;){k=this.text.charAt(f);if("("===k){g=c.substr(e-d+1);c=c.substr(0,e-d);this.index=f;break}if(this.isWhitespace(k))f++;else break}d={index:d,text:c};if(eb.hasOwnProperty(c))d.fn=eb[c],d.literal=!0,d.constant=!0;else{var m=Ec(c,this.options,
165 this.text);d.fn=B(function(a,c){return m(a,c)},{assign:function(d,e){return tb(d,c,e,a.text,a.options)}})}this.tokens.push(d);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+=
166 4,d+=String.fromCharCode(parseInt(f,16))):d+=Pe[g]||g,f=!1;else if("\\"===g)f=!0;else{if(g===a){this.index++;this.tokens.push({index:c,text:e,string:d,literal:!0,constant:!0,fn:function(){return d}});return}d+=g}this.index++}this.throwError("Unterminated quote",c)}};var db=function(a,c,d){this.lexer=a;this.$filter=c;this.options=d};db.ZERO=B(function(){return 0},{constant:!0});db.prototype={constructor:db,parse:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.statements();0!==this.tokens.length&&
167 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);a.literal=!!c.literal;a.constant=!!c.constant}for(var d;c=this.expect("(","[",".");)"("===c.text?(a=this.functionCall(a,d),d=null):
168 "["===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 ka("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},peekToken:function(){if(0===this.tokens.length)throw ka("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,
169 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 B(function(d,e){return a(d,e,c)},{constant:c.constant})},ternaryFn:function(a,c,d){return B(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 B(function(e,f){return c(e,f,a,d)},{constant:a.constant&&d.constant})},statements:function(){for(var a=[];;)if(0<this.tokens.length&&
170 !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,k){k=[k];for(var m=0;m<d.length;m++)k.push(d[m](a,
171 e));return c.apply(a,k)};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.assignment();if(d=this.expect(":"))return this.ternaryFn(a,c,this.assignment());
172 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("<",
173 ">","<=",">="))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(db.ZERO,a.fn,this.unary()):(a=this.expect("!"))?this.unaryFn(a.fn,this.unary()):
174 this.primary()},fieldAccess:function(a){var c=this,d=this.expect().text,e=Ec(d,this.options,this.text);return B(function(c,d,k){return e(k||a(c,d))},{assign:function(e,g,k){(k=a(e,k))||a.assign(e,k={});return tb(k,d,g,c.text,c.options)}})},objectIndex:function(a){var c=this,d=this.expression();this.consume("]");return B(function(e,f){var g=a(e,f),k=d(e,f),m;ja(k,c.text);if(!g)return t;(g=Oa(g[k],c.text))&&(g.then&&c.options.unwrapPromises)&&(m=g,"$$v"in g||(m.$$v=t,m.then(function(a){m.$$v=a})),g=
175 g.$$v);return g},{assign:function(e,f,g){var k=ja(d(e,g),c.text);(g=Oa(a(e,g),c.text))||a.assign(e,g={});return g[k]=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 k=[],m=c?c(f,g):f,h=0;h<d.length;h++)k.push(d[h](f,g));h=a(f,g,m)||y;Oa(m,e.text);var l=e.text;if(h){if(h.constructor===h)throw ka("isecfn",l);if(h===Ne||h===Oe||Qc&&h===Qc)throw ka("isecff",l);}k=h.apply?
176 h.apply(m,k):h(k[0],k[1],k[2],k[3],k[4]);return Oa(k,e.text)}},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 B(function(c,d){for(var g=[],k=0;k<a.length;k++)g.push(a[k](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(":");
177 var e=this.expression();a.push({key:d,value:e});e.constant||(c=!1)}while(this.expect(","))}this.consume("}");return B(function(c,d){for(var e={},m=0;m<a.length;m++){var h=a[m];e[h.key]=h.value(c,d)}return e},{literal:!0,constant:c})}};var Vb={},wa=x("$sce"),fa={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},V=X.createElement("a"),Hc=ua(Q.location.href,!0);mc.$inject=["$provide"];Ic.$inject=["$locale"];Kc.$inject=["$locale"];var Nc=".",He={yyyy:Y("FullYear",4),yy:Y("FullYear",
178 2,0,!0),y:Y("FullYear",1),MMMM:ub("Month"),MMM:ub("Month",!0),MM:Y("Month",2,1),M:Y("Month",1,1),dd:Y("Date",2),d:Y("Date",1),HH:Y("Hours",2),H:Y("Hours",1),hh:Y("Hours",2,-12),h:Y("Hours",1,-12),mm:Y("Minutes",2),m:Y("Minutes",1),ss:Y("Seconds",2),s:Y("Seconds",1),sss:Y("Milliseconds",3),EEEE:ub("Day"),EEE:ub("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?"+":"")+(Xb(Math[0<a?"floor":"ceil"](a/60),2)+Xb(Math.abs(a%60),
179 2))}},Ge=/((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,Fe=/^\-?\d+$/;Jc.$inject=["$locale"];var De=$(N),Ee=$(Ia);Lc.$inject=["$parse"];var dd=$({restrict:"E",compile:function(a,c){8>=R&&(c.href||c.name||c.$set("href",""),a.append(X.createComment("IE fix")));if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){var f="[object SVGAnimatedString]"===ya.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(f)||a.preventDefault()})}}}),Fb={};r(ob,function(a,
180 c){if("multiple"!=a){var d=na("ng-"+c);Fb[d]=function(){return{priority:100,link:function(a,f,g){a.$watch(g[d],function(a){g.$set(c,!!a)})}}}}});r(["src","srcset","href"],function(a){var c=na("ng-"+a);Fb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,k=a;"href"===a&&"[object SVGAnimatedString]"===ya.call(e.prop("href"))&&(k="xlinkHref",f.$attr[k]="xlink:href",g=null);f.$observe(c,function(c){c?(f.$set(k,c),R&&g&&e.prop(g,f[k])):"href"===a&&f.$set(k,null)})}}}});var xb={$addControl:y,
181 $removeControl:y,$setValidity:y,$setDirty:y,$setPristine:y};Oc.$inject=["$element","$attrs","$scope","$animate"];var Rc=function(a){return["$timeout",function(c){return{name:"form",restrict:a?"EAC":"E",controller:Oc,compile:function(){return{pre:function(a,e,f,g){if(!f.action){var k=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};rb(e[0],"submit",k);e.on("$destroy",function(){c(function(){Za(e[0],"submit",k)},0,!1)})}var m=e.parent().controller("form"),h=f.name||f.ngForm;h&&tb(a,
182 h,g,h);if(m)e.on("$destroy",function(){m.$removeControl(g);h&&tb(a,h,t,h);B(g,xb)})}}}}}]},ed=Rc(),rd=Rc(!0),Qe=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,Re=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,Se=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,Sc={text:zb,number:function(a,c,d,e,f,g){zb(a,c,d,e,f,g);e.$parsers.push(function(a){var c=e.$isEmpty(a);if(c||Se.test(a))return e.$setValidity("number",!0),""===
183 a?null:c?a:parseFloat(a);e.$setValidity("number",!1);return t});Ie(e,"number",Te,null,e.$$validityState);e.$formatters.push(function(a){return e.$isEmpty(a)?"":""+a});d.min&&(a=function(a){var c=parseFloat(d.min);return ra(e,"min",e.$isEmpty(a)||a>=c,a)},e.$parsers.push(a),e.$formatters.push(a));d.max&&(a=function(a){var c=parseFloat(d.max);return ra(e,"max",e.$isEmpty(a)||a<=c,a)},e.$parsers.push(a),e.$formatters.push(a));e.$formatters.push(function(a){return ra(e,"number",e.$isEmpty(a)||Ab(a),a)})},
184 url:function(a,c,d,e,f,g){zb(a,c,d,e,f,g);a=function(a){return ra(e,"url",e.$isEmpty(a)||Qe.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,f,g){zb(a,c,d,e,f,g);a=function(a){return ra(e,"email",e.$isEmpty(a)||Re.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){D(d.name)&&c.attr("name",gb());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",
185 e.$render)},checkbox:function(a,c,d,e){var f=d.ngTrueValue,g=d.ngFalseValue;z(f)||(f=!0);z(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:y,button:y,submit:y,reset:y,file:y},Te=["badInput"],jc=["$browser","$sniffer",function(a,c){return{restrict:"E",require:"?ngModel",link:function(d,
186 e,f,g){g&&(Sc[N(f.type)]||Sc.text)(d,e,f,g,c,a)}}}],wb="ng-valid",vb="ng-invalid",Pa="ng-pristine",yb="ng-dirty",Ue=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate",function(a,c,d,e,f,g){function k(a,c){c=c?"-"+kb(c,"-"):"";g.removeClass(e,(a?vb:wb)+c);g.addClass(e,(a?wb:vb)+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 m=f(d.ngModel),
187 h=m.assign;if(!h)throw x("ngModel")("nonassign",d.ngModel,ha(e));this.$render=y;this.$isEmpty=function(a){return D(a)||""===a||null===a||a!==a};var l=e.inheritedData("$formController")||xb,n=0,p=this.$error={};e.addClass(Pa);k(!0);this.$setValidity=function(a,c){p[a]!==!c&&(c?(p[a]&&n--,n||(k(!0),this.$valid=!0,this.$invalid=!1)):(k(!1),this.$invalid=!0,this.$valid=!1,n++),p[a]=!c,k(c,a),l.$setValidity(a,c,this))};this.$setPristine=function(){this.$dirty=!1;this.$pristine=!0;g.removeClass(e,yb);g.addClass(e,
188 Pa)};this.$setViewValue=function(d){this.$viewValue=d;this.$pristine&&(this.$dirty=!0,this.$pristine=!1,g.removeClass(e,Pa),g.addClass(e,yb),l.$setDirty());r(this.$parsers,function(a){d=a(d)});this.$modelValue!==d&&(this.$modelValue=d,h(a,d),r(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}}))};var q=this;a.$watch(function(){var c=m(a);if(q.$modelValue!==c){var d=q.$formatters,e=d.length;for(q.$modelValue=c;e--;)c=d[e](c);q.$viewValue!==c&&(q.$viewValue=c,q.$render())}return c})}],Gd=
189 function(){return{require:["ngModel","^?form"],controller:Ue,link:function(a,c,d,e){var f=e[0],g=e[1]||xb;g.$addControl(f);a.$on("$destroy",function(){g.$removeControl(f)})}}},Id=$({require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),kc=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);
190 e.$parsers.unshift(f);d.$observe("required",function(){f(e.$viewValue)})}}}},Hd=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(!D(a)){var c=[];a&&r(a.split(f),function(a){a&&c.push(aa(a))});return c}});e.$formatters.push(function(a){return H(a)?a.join(", "):t});e.$isEmpty=function(a){return!a||!a.length}}}},Ve=/^(true|false|\d+)$/,Jd=function(){return{priority:100,compile:function(a,c){return Ve.test(c.ngValue)?
191 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)})}}}},jd=xa({compile:function(a){a.addClass("ng-binding");return function(a,d,e){d.data("$binding",e.ngBind);a.$watch(e.ngBind,function(a){d.text(a==t?"":a)})}}}),ld=["$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)})}}],kd=["$sce","$parse",function(a,c){return{compile:function(d){d.addClass("ng-binding");
192 return function(d,f,g){f.data("$binding",g.ngBindHtml);var k=c(g.ngBindHtml);d.$watch(function(){return(k(d)||"").toString()},function(c){f.html(a.getTrustedHtml(k(d))||"")})}}}}],md=Yb("",!0),od=Yb("Odd",0),nd=Yb("Even",1),pd=xa({compile:function(a,c){c.$set("ngCloak",t);a.removeClass("ng-cloak")}}),qd=[function(){return{scope:!0,controller:"@",priority:500}}],lc={};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),
193 function(a){var c=na("ng-"+a);lc[c]=["$parse",function(d){return{compile:function(e,f){var g=d(f[c]);return function(c,d){d.on(N(a),function(a){c.$apply(function(){g(c,{$event:a})})})}}}}]});var td=["$animate",function(a){return{transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,f,g){var k,m,h;c.$watch(e.ngIf,function(f){Ta(f)?m||(m=c.$new(),g(m,function(c){c[c.length++]=X.createComment(" end ngIf: "+e.ngIf+" ");k={clone:c};a.enter(c,d.parent(),d)})):(h&&(h.remove(),
194 h=null),m&&(m.$destroy(),m=null),k&&(h=Eb(k.clone),a.leave(h,function(){h=null}),k=null))})}}}],ud=["$http","$templateCache","$anchorScroll","$animate","$sce",function(a,c,d,e,f){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:Ua.noop,compile:function(g,k){var m=k.ngInclude||k.src,h=k.onload||"",l=k.autoscroll;return function(g,k,q,r,L){var v=0,t,u,I,w=function(){u&&(u.remove(),u=null);t&&(t.$destroy(),t=null);I&&(e.leave(I,function(){u=null}),u=I,I=null)};g.$watch(f.parseAsResourceUrl(m),
195 function(f){var m=function(){!A(l)||l&&!g.$eval(l)||d()},q=++v;f?(a.get(f,{cache:c}).success(function(a){if(q===v){var c=g.$new();r.template=a;a=L(c,function(a){w();e.enter(a,null,k,m)});t=c;I=a;t.$emit("$includeContentLoaded");g.$eval(h)}}).error(function(){q===v&&w()}),g.$emit("$includeContentRequested")):(w(),r.template=null)})}}}}],Kd=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){d.html(f.template);a(d.contents())(c)}}}],vd=xa({priority:450,
196 compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),wd=xa({terminal:!0,priority:1E3}),xd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA",link:function(e,f,g){var k=g.count,m=g.$attr.when&&f.attr(g.$attr.when),h=g.offset||0,l=e.$eval(m)||{},n={},p=c.startSymbol(),q=c.endSymbol(),s=/^when(Minus)?(.+)$/;r(g,function(a,c){s.test(c)&&(l[N(c.replace("when","").replace("Minus","-"))]=f.attr(g.$attr[c]))});r(l,function(a,e){n[e]=c(a.replace(d,p+k+"-"+h+q))});e.$watch(function(){var c=
197 parseFloat(e.$eval(k));if(isNaN(c))return"";c in l||(c=a.pluralCat(c-h));return n[c](e,f,!0)},function(a){f.text(a)})}}}],yd=["$parse","$animate",function(a,c){var d=x("ngRepeat");return{transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,link:function(e,f,g,k,m){var h=g.ngRepeat,l=h.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),n,p,q,s,t,v,C={$id:Ka};if(!l)throw d("iexp",h);g=l[1];k=l[2];(l=l[3])?(n=a(l),p=function(a,c,d){v&&(C[v]=a);C[t]=c;C.$index=d;return n(e,
198 C)}):(q=function(a,c){return Ka(c)},s=function(a){return a});l=g.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!l)throw d("iidexp",g);t=l[3]||l[1];v=l[2];var A={};e.$watchCollection(k,function(a){var g,k,l=f[0],n,C={},J,E,F,x,z,y,H=[];if(fb(a))z=a,n=p||q;else{n=p||s;z=[];for(F in a)a.hasOwnProperty(F)&&"$"!=F.charAt(0)&&z.push(F);z.sort()}J=z.length;k=H.length=z.length;for(g=0;g<k;g++)if(F=a===z?g:z[g],x=a[F],x=n(F,x,g),Ca(x,"`track by` id"),A.hasOwnProperty(x))y=A[x],delete A[x],C[x]=
199 y,H[g]=y;else{if(C.hasOwnProperty(x))throw r(H,function(a){a&&a.scope&&(A[a.id]=a)}),d("dupes",h,x);H[g]={id:x};C[x]=!1}for(F in A)A.hasOwnProperty(F)&&(y=A[F],g=Eb(y.clone),c.leave(g),r(g,function(a){a.$$NG_REMOVED=!0}),y.scope.$destroy());g=0;for(k=z.length;g<k;g++){F=a===z?g:z[g];x=a[F];y=H[g];H[g-1]&&(l=H[g-1].clone[H[g-1].clone.length-1]);if(y.scope){E=y.scope;n=l;do n=n.nextSibling;while(n&&n.$$NG_REMOVED);y.clone[0]!=n&&c.move(Eb(y.clone),null,u(l));l=y.clone[y.clone.length-1]}else E=e.$new();
200 E[t]=x;v&&(E[v]=F);E.$index=g;E.$first=0===g;E.$last=g===J-1;E.$middle=!(E.$first||E.$last);E.$odd=!(E.$even=0===(g&1));y.scope||m(E,function(a){a[a.length++]=X.createComment(" end ngRepeat: "+h+" ");c.enter(a,null,u(l));l=a;y.scope=E;y.clone=a;C[y.id]=y})}A=C})}}}],zd=["$animate",function(a){return function(c,d,e){c.$watch(e.ngShow,function(c){a[Ta(c)?"removeClass":"addClass"](d,"ng-hide")})}}],sd=["$animate",function(a){return function(c,d,e){c.$watch(e.ngHide,function(c){a[Ta(c)?"addClass":"removeClass"](d,
201 "ng-hide")})}}],Ad=xa(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)}),Bd=["$animate",function(a){return{restrict:"EA",require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,d,e,f){var g=[],k=[],m=[],h=[];c.$watch(e.ngSwitch||e.on,function(d){var n,p;n=0;for(p=m.length;n<p;++n)m[n].remove();n=m.length=0;for(p=h.length;n<p;++n){var q=k[n];h[n].$destroy();m[n]=q;a.leave(q,function(){m.splice(n,1)})}k.length=0;h.length=
202 0;if(g=f.cases["!"+d]||f.cases["?"])c.$eval(e.change),r(g,function(d){var e=c.$new();h.push(e);d.transclude(e,function(c){var e=d.element;k.push(c);a.enter(c,e.parent(),e)})})})}}}],Cd=xa({transclude:"element",priority:800,require:"^ngSwitch",link:function(a,c,d,e,f){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cases["!"+d.ngSwitchWhen].push({transclude:f,element:c})}}),Dd=xa({transclude:"element",priority:800,require:"^ngSwitch",link:function(a,c,d,e,f){e.cases["?"]=e.cases["?"]||
203 [];e.cases["?"].push({transclude:f,element:c})}}),Fd=xa({link:function(a,c,d,e,f){if(!f)throw x("ngTransclude")("orphan",ha(c));f(function(a){c.empty();c.append(a)})}}),fd=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],We=x("ngOptions"),Ed=$({terminal:!0}),gd=["$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]+?))?$/,
204 e={$setViewValue:y};return{restrict:"E",require:["select","?ngModel"],controller:["$element","$scope","$attrs",function(a,c,d){var m=this,h={},l=e,n;m.databound=d.ngModel;m.init=function(a,c,d){l=a;n=d};m.addOption=function(c){Ca(c,'"option value"');h[c]=!0;l.$viewValue==c&&(a.val(c),n.parent()&&n.remove())};m.removeOption=function(a){this.hasOption(a)&&(delete h[a],l.$viewValue==a&&this.renderUnknownOption(a))};m.renderUnknownOption=function(c){c="? "+Ka(c)+" ?";n.val(c);a.prepend(n);a.val(c);n.prop("selected",
205 !0)};m.hasOption=function(a){return h.hasOwnProperty(a)};c.$on("$destroy",function(){m.renderUnknownOption=y})}],link:function(e,g,k,m){function h(a,c,d,e){d.$render=function(){var a=d.$viewValue;e.hasOption(a)?(z.parent()&&z.remove(),c.val(a),""===a&&v.prop("selected",!0)):D(a)&&v?c.val(""):e.renderUnknownOption(a)};c.on("change",function(){a.$apply(function(){z.parent()&&z.remove();d.$setViewValue(c.val())})})}function l(a,c,d){var e;d.$render=function(){var a=new ab(d.$viewValue);r(c.find("option"),
206 function(c){c.selected=A(a.get(c.value))})};a.$watch(function(){za(e,d.$viewValue)||(e=ga(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 n(e,f,g){function k(){var a={"":[]},c=[""],d,h,s,t,w;s=g.$modelValue;t=v(e)||[];var E=n?Zb(t):t,I,M,B;M={};B=!1;if(q)if(h=g.$modelValue,u&&H(h))for(B=new ab([]),d={},w=0;w<h.length;w++)d[m]=h[w],B.put(u(e,d),h[w]);else B=new ab(h);w=B;
207 var D,J;for(B=0;I=E.length,B<I;B++){h=B;if(n){h=E[B];if("$"===h.charAt(0))continue;M[n]=h}M[m]=t[h];d=p(e,M)||"";(h=a[d])||(h=a[d]=[],c.push(d));q?d=A(w.remove(u?u(e,M):r(e,M))):(u?(d={},d[m]=s,d=u(e,d)===u(e,M)):d=s===r(e,M),w=w||d);D=l(e,M);D=A(D)?D:"";h.push({id:u?u(e,M):n?E[B]:B,label:D,selected:d})}q||(x||null===s?a[""].unshift({id:"",label:"",selected:!w}):w||a[""].unshift({id:"?",label:"",selected:!0}));M=0;for(E=c.length;M<E;M++){d=c[M];h=a[d];z.length<=M?(s={element:y.clone().attr("label",
208 d),label:h.label},t=[s],z.push(t),f.append(s.element)):(t=z[M],s=t[0],s.label!=d&&s.element.attr("label",s.label=d));D=null;B=0;for(I=h.length;B<I;B++)d=h[B],(w=t[B+1])?(D=w.element,w.label!==d.label&&D.text(w.label=d.label),w.id!==d.id&&D.val(w.id=d.id),D[0].selected!==d.selected&&(D.prop("selected",w.selected=d.selected),R&&D.prop("selected",w.selected))):(""===d.id&&x?J=x:(J=C.clone()).val(d.id).prop("selected",d.selected).attr("selected",d.selected).text(d.label),t.push({element:J,label:d.label,
209 id:d.id,selected:d.selected}),D?D.after(J):s.element.append(J),D=J);for(B++;t.length>B;)t.pop().element.remove()}for(;z.length>M;)z.pop()[0].element.remove()}var h;if(!(h=s.match(d)))throw We("iexp",s,ha(f));var l=c(h[2]||h[1]),m=h[4]||h[6],n=h[5],p=c(h[3]||""),r=c(h[2]?h[1]:m),v=c(h[7]),u=h[8]?c(h[8]):null,z=[[{element:f,label:""}]];x&&(a(x)(e),x.removeClass("ng-scope"),x.remove());f.empty();f.on("change",function(){e.$apply(function(){var a,c=v(e)||[],d={},h,l,p,s,w,x,y;if(q)for(l=[],s=0,x=z.length;s<
210 x;s++)for(a=z[s],p=1,w=a.length;p<w;p++){if((h=a[p].element)[0].selected){h=h.val();n&&(d[n]=h);if(u)for(y=0;y<c.length&&(d[m]=c[y],u(e,d)!=h);y++);else d[m]=c[h];l.push(r(e,d))}}else if(h=f.val(),"?"==h)l=t;else if(""===h)l=null;else if(u)for(y=0;y<c.length;y++){if(d[m]=c[y],u(e,d)==h){l=r(e,d);break}}else d[m]=c[h],n&&(d[n]=h),l=r(e,d);g.$setViewValue(l);k()})});g.$render=k;e.$watchCollection(v,k);q&&e.$watchCollection(function(){return g.$modelValue},k)}if(m[1]){var p=m[0];m=m[1];var q=k.multiple,
211 s=k.ngOptions,x=!1,v,C=u(X.createElement("option")),y=u(X.createElement("optgroup")),z=C.clone();k=0;for(var w=g.children(),B=w.length;k<B;k++)if(""===w[k].value){v=x=w.eq(k);break}p.init(m,x,z);q&&(m.$isEmpty=function(a){return!a||0===a.length});s?n(e,g,m):q?l(e,g,m):h(e,g,m,p)}}}}],id=["$interpolate",function(a){var c={addOption:y,removeOption:y};return{restrict:"E",priority:100,compile:function(d,e){if(D(e.value)){var f=a(d.text(),!0);f||e.$set("value",d.text())}return function(a,d,e){var h=d.parent(),
212 l=h.data("$selectController")||h.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)})}}}}],hd=$({restrict:"E",terminal:!0});Q.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):((Da=Q.jQuery)&&Da.fn.on?(u=Da,B(Da.fn,{scope:La.scope,isolateScope:La.isolateScope,controller:La.controller,injector:La.injector,
213 inheritedData:La.inheritedData}),Gb("remove",!0,!0,!1),Gb("empty",!1,!1,!1),Gb("html",!1,!1,!0)):u=S,Ua.element=u,$c(Ua),u(X).ready(function(){Xc(X,fc)}))})(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>');
5 (function(O,U,t){'use strict';function J(b){return function(){var a=arguments[0],c;c="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.4.3/"+(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 Ea(b){if(null==b||Wa(b))return!1;var a="length"in Object(b)&&b.length;
6 return b.nodeType===qa&&a?!0:L(b)||G(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function m(b,a,c){var d,e;if(b)if(z(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(G(b)||Ea(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!==m)b.forEach(a,c,b);else if(nc(b))for(d in b)a.call(c,b[d],d,b);else if("function"===typeof b.hasOwnProperty)for(d in b)b.hasOwnProperty(d)&&
7 a.call(c,b[d],d,b);else for(d in b)Xa.call(b,d)&&a.call(c,b[d],d,b);return b}function oc(b,a,c){for(var d=Object.keys(b).sort(),e=0;e<d.length;e++)a.call(c,b[d[e]],d[e]);return d}function pc(b){return function(a,c){b(c,a)}}function Ud(){return++nb}function qc(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function Nb(b,a,c){for(var d=b.$$hashKey,e=0,f=a.length;e<f;++e){var g=a[e];if(H(g)||z(g))for(var h=Object.keys(g),l=0,k=h.length;l<k;l++){var n=h[l],r=g[n];c&&H(r)?aa(r)?b[n]=new Date(r.valueOf()):(H(b[n])||
8 (b[n]=G(r)?[]:{}),Nb(b[n],[r],!0)):b[n]=r}}qc(b,d);return b}function P(b){return Nb(b,za.call(arguments,1),!1)}function Vd(b){return Nb(b,za.call(arguments,1),!0)}function W(b){return parseInt(b,10)}function Ob(b,a){return P(Object.create(b),a)}function v(){}function Ya(b){return b}function ra(b){return function(){return b}}function rc(b){return z(b.toString)&&b.toString!==Object.prototype.toString}function A(b){return"undefined"===typeof b}function w(b){return"undefined"!==typeof b}function H(b){return null!==
9 b&&"object"===typeof b}function nc(b){return null!==b&&"object"===typeof b&&!sc(b)}function L(b){return"string"===typeof b}function V(b){return"number"===typeof b}function aa(b){return"[object Date]"===sa.call(b)}function z(b){return"function"===typeof b}function Za(b){return"[object RegExp]"===sa.call(b)}function Wa(b){return b&&b.window===b}function $a(b){return b&&b.$evalAsync&&b.$watch}function ab(b){return"boolean"===typeof b}function tc(b){return!(!b||!(b.nodeName||b.prop&&b.attr&&b.find))}
10 function Wd(b){var a={};b=b.split(",");var c;for(c=0;c<b.length;c++)a[b[c]]=!0;return a}function ta(b){return M(b.nodeName||b[0]&&b[0].nodeName)}function bb(b,a){var c=b.indexOf(a);0<=c&&b.splice(c,1);return c}function fa(b,a,c,d){if(Wa(b)||$a(b))throw Fa("cpws");if(uc.test(sa.call(a)))throw Fa("cpta");if(a){if(b===a)throw Fa("cpi");c=c||[];d=d||[];H(b)&&(c.push(b),d.push(a));var e;if(G(b))for(e=a.length=0;e<b.length;e++)a.push(fa(b[e],null,c,d));else{var f=a.$$hashKey;G(a)?a.length=0:m(a,function(b,
11 c){delete a[c]});if(nc(b))for(e in b)a[e]=fa(b[e],null,c,d);else if(b&&"function"===typeof b.hasOwnProperty)for(e in b)b.hasOwnProperty(e)&&(a[e]=fa(b[e],null,c,d));else for(e in b)Xa.call(b,e)&&(a[e]=fa(b[e],null,c,d));qc(a,f)}}else if(a=b,H(b)){if(c&&-1!==(f=c.indexOf(b)))return d[f];if(G(b))return fa(b,[],c,d);if(uc.test(sa.call(b)))a=new b.constructor(b);else if(aa(b))a=new Date(b.getTime());else if(Za(b))a=new RegExp(b.source,b.toString().match(/[^\/]*$/)[0]),a.lastIndex=b.lastIndex;else return e=
12 Object.create(sc(b)),fa(b,e,c,d);d&&(c.push(b),d.push(a))}return a}function ia(b,a){if(G(b)){a=a||[];for(var c=0,d=b.length;c<d;c++)a[c]=b[c]}else if(H(b))for(c in a=a||{},b)if("$"!==c.charAt(0)||"$"!==c.charAt(1))a[c]=b[c];return a||b}function ka(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(G(b)){if(!G(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!ka(b[d],a[d]))return!1;return!0}}else{if(aa(b))return aa(a)?
13 ka(b.getTime(),a.getTime()):!1;if(Za(b))return Za(a)?b.toString()==a.toString():!1;if($a(b)||$a(a)||Wa(b)||Wa(a)||G(a)||aa(a)||Za(a))return!1;c=ga();for(d in b)if("$"!==d.charAt(0)&&!z(b[d])){if(!ka(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!(d in c||"$"===d.charAt(0)||a[d]===t||z(a[d])))return!1;return!0}return!1}function cb(b,a,c){return b.concat(za.call(a,c))}function vc(b,a){var c=2<arguments.length?za.call(arguments,2):[];return!z(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?
14 a.apply(b,cb(c,arguments,0)):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function Xd(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)&&"$"===b.charAt(1)?c=t:Wa(a)?c="$WINDOW":a&&U===a?c="$DOCUMENT":$a(a)&&(c="$SCOPE");return c}function db(b,a){if("undefined"===typeof b)return t;V(a)||(a=a?2:null);return JSON.stringify(b,Xd,a)}function wc(b){return L(b)?JSON.parse(b):b}function xc(b,a){var c=Date.parse("Jan 01, 1970 00:00:00 "+b)/6E4;return isNaN(c)?a:c}function Pb(b,
15 a,c){c=c?-1:1;var d=xc(a,b.getTimezoneOffset());a=b;b=c*(d-b.getTimezoneOffset());a=new Date(a.getTime());a.setMinutes(a.getMinutes()+b);return a}function ua(b){b=y(b).clone();try{b.empty()}catch(a){}var c=y("<div>").append(b).html();try{return b[0].nodeType===Na?M(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,function(a,b){return"<"+M(b)})}catch(d){return M(c)}}function yc(b){try{return decodeURIComponent(b)}catch(a){}}function zc(b){var a={},c,d;m((b||"").split("&"),function(b){b&&(c=b.replace(/\+/g,
16 "%20").split("="),d=yc(c[0]),w(d)&&(b=w(c[1])?yc(c[1]):!0,Xa.call(a,d)?G(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function Qb(b){var a=[];m(b,function(b,d){G(b)?m(b,function(b){a.push(ma(d,!0)+(!0===b?"":"="+ma(b,!0)))}):a.push(ma(d,!0)+(!0===b?"":"="+ma(b,!0)))});return a.length?a.join("&"):""}function ob(b){return ma(b,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ma(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,
17 "$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,a?"%20":"+")}function Yd(b,a){var c,d,e=Oa.length;for(d=0;d<e;++d)if(c=Oa[d]+a,L(c=b.getAttribute(c)))return c;return null}function Zd(b,a){var c,d,e={};m(Oa,function(a){a+="app";!c&&b.hasAttribute&&b.hasAttribute(a)&&(c=b,d=b.getAttribute(a))});m(Oa,function(a){a+="app";var e;!c&&(e=b.querySelector("["+a.replace(":","\\:")+"]"))&&(c=e,d=e.getAttribute(a))});c&&(e.strictDi=null!==Yd(c,"strict-di"),a(c,d?[d]:[],e))}function Ac(b,a,c){H(c)||
18 (c={});c=P({strictDi:!1},c);var d=function(){b=y(b);if(b.injector()){var d=b[0]===U?"document":ua(b);throw Fa("btstrpd",d.replace(/</,"&lt;").replace(/>/,"&gt;"));}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=eb(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=
19 /^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;O&&e.test(O.name)&&(c.debugInfoEnabled=!0,O.name=O.name.replace(e,""));if(O&&!f.test(O.name))return d();O.name=O.name.replace(f,"");ca.resumeBootstrap=function(b){m(b,function(b){a.push(b)});return d()};z(ca.resumeDeferredBootstrap)&&ca.resumeDeferredBootstrap()}function $d(){O.name="NG_ENABLE_DEBUG_INFO!"+O.name;O.location.reload()}function ae(b){b=ca.element(b).injector();if(!b)throw Fa("test");return b.get("$$testability")}function Bc(b,a){a=a||
20 "_";return b.replace(be,function(b,d){return(d?a:"")+b.toLowerCase()})}function ce(){var b;if(!Cc){var a=pb();la=O.jQuery;w(a)&&(la=null===a?t:O[a]);la&&la.fn.on?(y=la,P(la.fn,{scope:Pa.scope,isolateScope:Pa.isolateScope,controller:Pa.controller,injector:Pa.injector,inheritedData:Pa.inheritedData}),b=la.cleanData,la.cleanData=function(a){var d;if(Rb)Rb=!1;else for(var e=0,f;null!=(f=a[e]);e++)(d=la._data(f,"events"))&&d.$destroy&&la(f).triggerHandler("$destroy");b(a)}):y=Q;ca.element=y;Cc=!0}}function Sb(b,
21 a,c){if(!b)throw Fa("areq",a||"?",c||"required");return b}function Qa(b,a,c){c&&G(b)&&(b=b[b.length-1]);Sb(z(b),a,"not a function, got "+(b&&"object"===typeof b?b.constructor.name||"Object":typeof b));return b}function Ra(b,a){if("hasOwnProperty"===b)throw Fa("badname",a);}function Dc(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&&z(b)?vc(e,b):b}function qb(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!==
22 b);return y(c)}function ga(){return Object.create(null)}function de(b){function a(a,b,c){return a[b]||(a[b]=c())}var c=J("$injector"),d=J("ng");b=a(b,"angular",Object);b.$$minErr=b.$$minErr||J;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(b,c,e,f){f||(f=d);return function(){f[e||"push"]([b,c,arguments]);return C}}function b(a,c){return function(b,e){e&&z(e)&&
23 (e.$$moduleName=f);d.push([a,c,arguments]);return C}}if(!g)throw c("nomod",f);var d=[],e=[],s=[],x=a("$injector","invoke","push",e),C={_invokeQueue:d,_configBlocks:e,_runBlocks:s,requires:g,name:f,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:a("$provide","value"),constant:a("$provide","constant","unshift"),decorator:b("$provide","decorator"),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider",
24 "register"),directive:b("$compileProvider","directive"),config:x,run:function(a){s.push(a);return this}};h&&x(h);return C})}})}function ee(b){P(b,{bootstrap:Ac,copy:fa,extend:P,merge:Vd,equals:ka,element:y,forEach:m,injector:eb,noop:v,bind:vc,toJson:db,fromJson:wc,identity:Ya,isUndefined:A,isDefined:w,isString:L,isFunction:z,isObject:H,isNumber:V,isElement:tc,isArray:G,version:fe,isDate:aa,lowercase:M,uppercase:rb,callbacks:{counter:0},getTestability:ae,$$minErr:J,$$csp:fb,reloadWithDebugInfo:$d});
25 gb=de(O);try{gb("ngLocale")}catch(a){gb("ngLocale",[]).provider("$locale",ge)}gb("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:he});a.provider("$compile",Ec).directive({a:ie,input:Fc,textarea:Fc,form:je,script:ke,select:le,style:me,option:ne,ngBind:oe,ngBindHtml:pe,ngBindTemplate:qe,ngClass:re,ngClassEven:se,ngClassOdd:te,ngCloak:ue,ngController:ve,ngForm:we,ngHide:xe,ngIf:ye,ngInclude:ze,ngInit:Ae,ngNonBindable:Be,ngPluralize:Ce,ngRepeat:De,ngShow:Ee,ngStyle:Fe,ngSwitch:Ge,
26 ngSwitchWhen:He,ngSwitchDefault:Ie,ngOptions:Je,ngTransclude:Ke,ngModel:Le,ngList:Me,ngChange:Ne,pattern:Gc,ngPattern:Gc,required:Hc,ngRequired:Hc,minlength:Ic,ngMinlength:Ic,maxlength:Jc,ngMaxlength:Jc,ngValue:Oe,ngModelOptions:Pe}).directive({ngInclude:Qe}).directive(sb).directive(Kc);a.provider({$anchorScroll:Re,$animate:Se,$$animateQueue:Te,$$AnimateRunner:Ue,$browser:Ve,$cacheFactory:We,$controller:Xe,$document:Ye,$exceptionHandler:Ze,$filter:Lc,$interpolate:$e,$interval:af,$http:bf,$httpParamSerializer:cf,
27 $httpParamSerializerJQLike:df,$httpBackend:ef,$location:ff,$log:gf,$parse:hf,$rootScope:jf,$q:kf,$$q:lf,$sce:mf,$sceDelegate:nf,$sniffer:of,$templateCache:pf,$templateRequest:qf,$$testability:rf,$timeout:sf,$window:tf,$$rAF:uf,$$jqLite:vf,$$HashMap:wf,$$cookieReader:xf})}])}function hb(b){return b.replace(yf,function(a,b,d,e){return e?d.toUpperCase():d}).replace(zf,"Moz$1")}function Mc(b){b=b.nodeType;return b===qa||!b||9===b}function Nc(b,a){var c,d,e=a.createDocumentFragment(),f=[];if(Tb.test(b)){c=
28 c||e.appendChild(a.createElement("div"));d=(Af.exec(b)||["",""])[1].toLowerCase();d=na[d]||na._default;c.innerHTML=d[1]+b.replace(Bf,"<$1></$2>")+d[2];for(d=d[0];d--;)c=c.lastChild;f=cb(f,c.childNodes);c=e.firstChild;c.textContent=""}else f.push(a.createTextNode(b));e.textContent="";e.innerHTML="";m(f,function(a){e.appendChild(a)});return e}function Q(b){if(b instanceof Q)return b;var a;L(b)&&(b=R(b),a=!0);if(!(this instanceof Q)){if(a&&"<"!=b.charAt(0))throw Ub("nosel");return new Q(b)}if(a){a=U;
29 var c;b=(c=Cf.exec(b))?[a.createElement(c[1])]:(c=Nc(b,a))?c.childNodes:[]}Oc(this,b)}function Vb(b){return b.cloneNode(!0)}function tb(b,a){a||ub(b);if(b.querySelectorAll)for(var c=b.querySelectorAll("*"),d=0,e=c.length;d<e;d++)ub(c[d])}function Pc(b,a,c,d){if(w(d))throw Ub("offargs");var e=(d=vb(b))&&d.events,f=d&&d.handle;if(f)if(a)m(a.split(" "),function(a){if(w(c)){var d=e[a];bb(d||[],c);if(d&&0<d.length)return}b.removeEventListener(a,f,!1);delete e[a]});else for(a in e)"$destroy"!==a&&b.removeEventListener(a,
30 f,!1),delete e[a]}function ub(b,a){var c=b.ng339,d=c&&ib[c];d&&(a?delete d.data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),Pc(b)),delete ib[c],b.ng339=t))}function vb(b,a){var c=b.ng339,c=c&&ib[c];a&&!c&&(b.ng339=c=++Df,c=ib[c]={events:{},data:{},handle:t});return c}function Wb(b,a,c){if(Mc(b)){var d=w(c),e=!d&&a&&!H(a),f=!a;b=(b=vb(b,!e))&&b.data;if(d)b[a]=c;else{if(f)return b;if(e)return b&&b[a];P(b,a)}}}function wb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||"")+
31 " ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function xb(b,a){a&&b.setAttribute&&m(a.split(" "),function(a){b.setAttribute("class",R((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+R(a)+" "," ")))})}function yb(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");m(a.split(" "),function(a){a=R(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",R(c))}}function Oc(b,a){if(a)if(a.nodeType)b[b.length++]=a;else{var c=
32 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 Qc(b,a){return zb(b,"$"+(a||"ngController")+"Controller")}function zb(b,a,c){9==b.nodeType&&(b=b.documentElement);for(a=G(a)?a:[a];b;){for(var d=0,e=a.length;d<e;d++)if((c=y.data(b,a[d]))!==t)return c;b=b.parentNode||11===b.nodeType&&b.host}}function Rc(b){for(tb(b,!0);b.firstChild;)b.removeChild(b.firstChild)}function Xb(b,a){a||tb(b);var c=b.parentNode;c&&c.removeChild(b)}function Ef(b,
33 a){a=a||O;if("complete"===a.document.readyState)a.setTimeout(b);else y(a).on("load",b)}function Sc(b,a){var c=Ab[a.toLowerCase()];return c&&Tc[ta(b)]&&c}function Ff(b,a){var c=b.nodeName;return("INPUT"===c||"TEXTAREA"===c)&&Uc[a]}function Gf(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){if(A(c.immediatePropagationStopped)){var h=c.stopImmediatePropagation;c.stopImmediatePropagation=function(){c.immediatePropagationStopped=
34 !0;c.stopPropagation&&c.stopPropagation();h&&h.call(c)}}c.isImmediatePropagationStopped=function(){return!0===c.immediatePropagationStopped};1<g&&(f=ia(f));for(var l=0;l<g;l++)c.isImmediatePropagationStopped()||f[l].call(b,c)}};c.elem=b;return c}function vf(){this.$get=function(){return P(Q,{hasClass:function(b,a){b.attr&&(b=b[0]);return wb(b,a)},addClass:function(b,a){b.attr&&(b=b[0]);return yb(b,a)},removeClass:function(b,a){b.attr&&(b=b[0]);return xb(b,a)}})}}function Ga(b,a){var c=b&&b.$$hashKey;
35 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||Ud)():c+":"+b}function Sa(b,a){if(a){var c=0;this.nextUid=function(){return++c}}m(b,this.put,this)}function Hf(b){return(b=b.toString().replace(Vc,"").match(Wc))?"function("+(b[1]||"").replace(/[\s\r\n]+/," ")+")":"fn"}function eb(b,a){function c(a){return function(b,c){if(H(b))m(b,pc(a));else return a(b,c)}}function d(a,b){Ra(a,"service");if(z(b)||G(b))b=s.instantiate(b);
36 if(!b.$get)throw Ha("pget",a);return r[a+"Provider"]=b}function e(a,b){return function(){var c=C.invoke(b,this);if(A(c))throw Ha("undef",a);return c}}function f(a,b,c){return d(a,{$get:!1!==c?e(a,b):b})}function g(a){var b=[],c;m(a,function(a){function d(a){var b,c;b=0;for(c=a.length;b<c;b++){var e=a[b],f=s.get(e[0]);f[e[1]].apply(f,e[2])}}if(!n.get(a)){n.put(a,!0);try{L(a)?(c=gb(a),b=b.concat(g(c.requires)).concat(c._runBlocks),d(c._invokeQueue),d(c._configBlocks)):z(a)?b.push(s.invoke(a)):G(a)?
37 b.push(s.invoke(a)):Qa(a,"module")}catch(e){throw G(a)&&(a=a[a.length-1]),e.message&&e.stack&&-1==e.stack.indexOf(e.message)&&(e=e.message+"\n"+e.stack),Ha("modulerr",a,e.stack||e.message||e);}}});return b}function h(b,c){function d(a,e){if(b.hasOwnProperty(a)){if(b[a]===l)throw Ha("cdep",a+" <- "+k.join(" <- "));return b[a]}try{return k.unshift(a),b[a]=l,b[a]=c(a,e)}catch(f){throw b[a]===l&&delete b[a],f;}finally{k.shift()}}function e(b,c,f,g){"string"===typeof f&&(g=f,f=null);var h=[],k=eb.$$annotate(b,
38 a,g),l,s,n;s=0;for(l=k.length;s<l;s++){n=k[s];if("string"!==typeof n)throw Ha("itkn",n);h.push(f&&f.hasOwnProperty(n)?f[n]:d(n,g))}G(b)&&(b=b[l]);return b.apply(c,h)}return{invoke:e,instantiate:function(a,b,c){var d=Object.create((G(a)?a[a.length-1]:a).prototype||null);a=e(a,d,b,c);return H(a)||z(a)?a:d},get:d,annotate:eb.$$annotate,has:function(a){return r.hasOwnProperty(a+"Provider")||b.hasOwnProperty(a)}}}a=!0===a;var l={},k=[],n=new Sa([],!0),r={$provide:{provider:c(d),factory:c(f),service:c(function(a,
39 b){return f(a,["$injector",function(a){return a.instantiate(b)}])}),value:c(function(a,b){return f(a,ra(b),!1)}),constant:c(function(a,b){Ra(a,"constant");r[a]=b;x[a]=b}),decorator:function(a,b){var c=s.get(a+"Provider"),d=c.$get;c.$get=function(){var a=C.invoke(d,c);return C.invoke(b,null,{$delegate:a})}}}},s=r.$injector=h(r,function(a,b){ca.isString(b)&&k.push(b);throw Ha("unpr",k.join(" <- "));}),x={},C=x.$injector=h(x,function(a,b){var c=s.get(a+"Provider",b);return C.invoke(c.$get,c,t,a)});m(g(b),
40 function(a){a&&C.invoke(a)});return C}function Re(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;Array.prototype.some.call(a,function(a){if("a"===ta(a))return b=a,!0});return b}function f(b){if(b){b.scrollIntoView();var c;c=g.yOffset;z(c)?c=c():tc(c)?(c=c[0],c="fixed"!==a.getComputedStyle(c).position?0:c.getBoundingClientRect().bottom):V(c)||(c=0);c&&(b=b.getBoundingClientRect().top,a.scrollBy(0,b-c))}else a.scrollTo(0,
41 0)}function g(a){a=L(a)?a:c.hash();var b;a?(b=h.getElementById(a))?f(b):(b=e(h.getElementsByName(a)))?f(b):"top"===a&&f(null):f(null)}var h=a.document;b&&d.$watch(function(){return c.hash()},function(a,b){a===b&&""===a||Ef(function(){d.$evalAsync(g)})});return g}]}function jb(b,a){if(!b&&!a)return"";if(!b)return a;if(!a)return b;G(b)&&(b=b.join(" "));G(a)&&(a=a.join(" "));return b+" "+a}function If(b){L(b)&&(b=b.split(" "));var a=ga();m(b,function(b){b.length&&(a[b]=!0)});return a}function Ia(b){return H(b)?
42 b:{}}function Jf(b,a,c,d){function e(a){try{a.apply(null,za.call(arguments,1))}finally{if(C--,0===C)for(;F.length;)try{F.pop()()}catch(b){c.error(b)}}}function f(){g();h()}function g(){a:{try{u=n.state;break a}catch(a){}u=void 0}u=A(u)?null:u;ka(u,D)&&(u=D);D=u}function h(){if(K!==l.url()||p!==u)K=l.url(),p=u,m(B,function(a){a(l.url(),u)})}var l=this,k=b.location,n=b.history,r=b.setTimeout,s=b.clearTimeout,x={};l.isMock=!1;var C=0,F=[];l.$$completeOutstandingRequest=e;l.$$incOutstandingRequestCount=
43 function(){C++};l.notifyWhenNoOutstandingRequests=function(a){0===C?a():F.push(a)};var u,p,K=k.href,q=a.find("base"),I=null;g();p=u;l.url=function(a,c,e){A(e)&&(e=null);k!==b.location&&(k=b.location);n!==b.history&&(n=b.history);if(a){var f=p===e;if(K===a&&(!d.history||f))return l;var h=K&&Ja(K)===Ja(a);K=a;p=e;if(!d.history||h&&f){if(!h||I)I=a;c?k.replace(a):h?(c=k,e=a.indexOf("#"),a=-1===e?"":a.substr(e),c.hash=a):k.href=a}else n[c?"replaceState":"pushState"](e,"",a),g(),p=u;return l}return I||
44 k.href.replace(/%27/g,"'")};l.state=function(){return u};var B=[],N=!1,D=null;l.onUrlChange=function(a){if(!N){if(d.history)y(b).on("popstate",f);y(b).on("hashchange",f);N=!0}B.push(a);return a};l.$$applicationDestroyed=function(){y(b).off("hashchange popstate",f)};l.$$checkUrlChange=h;l.baseHref=function(){var a=q.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};l.defer=function(a,b){var c;C++;c=r(function(){delete x[c];e(a)},b||0);x[c]=!0;return c};l.defer.cancel=function(a){return x[a]?
45 (delete x[a],s(a),e(v),!0):!1}}function Ve(){this.$get=["$window","$log","$sniffer","$document",function(b,a,c,d){return new Jf(b,d,a,c)}]}function We(){this.$get=function(){function b(b,d){function e(a){a!=r&&(s?s==a&&(s=a.n):s=a,f(a.n,a.p),f(a,r),r=a,r.n=null)}function f(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(b in a)throw J("$cacheFactory")("iid",b);var g=0,h=P({},d,{id:b}),l={},k=d&&d.capacity||Number.MAX_VALUE,n={},r=null,s=null;return a[b]={put:function(a,b){if(!A(b)){if(k<Number.MAX_VALUE){var c=
46 n[a]||(n[a]={key:a});e(c)}a in l||g++;l[a]=b;g>k&&this.remove(s.key);return b}},get:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;e(b)}return l[a]},remove:function(a){if(k<Number.MAX_VALUE){var b=n[a];if(!b)return;b==r&&(r=b.p);b==s&&(s=b.n);f(b.n,b.p);delete n[a]}delete l[a];g--},removeAll:function(){l={};g=0;n={};r=s=null},destroy:function(){n=h=l=null;delete a[b]},info:function(){return P({},h,{size:g})}}}var a={};b.info=function(){var b={};m(a,function(a,e){b[e]=a.info()});return b};
47 b.get=function(b){return a[b]};return b}}function pf(){this.$get=["$cacheFactory",function(b){return b("templates")}]}function Ec(b,a){function c(a,b,c){var d=/^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/,e={};m(a,function(a,f){var g=a.match(d);if(!g)throw ea("iscp",b,f,a,c?"controller bindings definition":"isolate scope definition");e[f]={mode:g[1][0],collection:"*"===g[2],optional:"?"===g[3],attrName:g[4]||f}});return e}function d(a){var b=a.charAt(0);if(!b||b!==M(b))throw ea("baddir",a);if(a!==a.trim())throw ea("baddir",
48 a);}var e={},f=/^\s*directive\:\s*([\w\-]+)\s+(.*)$/,g=/(([\w\-]+)(?:\:([^;]+))?;?)/,h=Wd("ngSrc,ngSrcset,src,srcset"),l=/^(?:(\^\^?)?(\?)?(\^\^?)?)?/,k=/^(on[a-z]+|formaction)$/;this.directive=function s(a,f){Ra(a,"directive");L(a)?(d(a),Sb(f,"directiveFactory"),e.hasOwnProperty(a)||(e[a]=[],b.factory(a+"Directive",["$injector","$exceptionHandler",function(b,d){var f=[];m(e[a],function(e,g){try{var h=b.invoke(e);z(h)?h={compile:ra(h)}:!h.compile&&h.link&&(h.compile=ra(h.link));h.priority=h.priority||
49 0;h.index=g;h.name=h.name||a;h.require=h.require||h.controller&&h.name;h.restrict=h.restrict||"EA";var k=h,l=h,s=h.name,n={isolateScope:null,bindToController:null};H(l.scope)&&(!0===l.bindToController?(n.bindToController=c(l.scope,s,!0),n.isolateScope={}):n.isolateScope=c(l.scope,s,!1));H(l.bindToController)&&(n.bindToController=c(l.bindToController,s,!0));if(H(n.bindToController)){var C=l.controller,$=l.controllerAs;if(!C)throw ea("noctrl",s);var ha;a:if($&&L($))ha=$;else{if(L(C)){var m=Xc.exec(C);
50 if(m){ha=m[3];break a}}ha=void 0}if(!ha)throw ea("noident",s);}var q=k.$$bindings=n;H(q.isolateScope)&&(h.$$isolateBindings=q.isolateScope);h.$$moduleName=e.$$moduleName;f.push(h)}catch(t){d(t)}});return f}])),e[a].push(f)):m(a,pc(s));return this};this.aHrefSanitizationWhitelist=function(b){return w(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(b){return w(b)?(a.imgSrcSanitizationWhitelist(b),this):a.imgSrcSanitizationWhitelist()};
51 var n=!0;this.debugInfoEnabled=function(a){return w(a)?(n=a,this):n};this.$get=["$injector","$interpolate","$exceptionHandler","$templateRequest","$parse","$controller","$rootScope","$document","$sce","$animate","$$sanitizeUri",function(a,b,c,d,u,p,K,q,I,B,N){function D(a,b){try{a.addClass(b)}catch(c){}}function Z(a,b,c,d,e){a instanceof y||(a=y(a));m(a,function(b,c){b.nodeType==Na&&b.nodeValue.match(/\S+/)&&(a[c]=y(b).wrap("<span></span>").parent()[0])});var f=S(a,b,a,c,d,e);Z.$$addScopeClass(a);
52 var g=null;return function(b,c,d){Sb(b,"scope");d=d||{};var e=d.parentBoundTranscludeFn,h=d.transcludeControllers;d=d.futureParentElement;e&&e.$$boundTransclude&&(e=e.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ta(d)&&d.toString().match(/SVG/)?"svg":"html":"html");d="html"!==g?y(Yb(g,y("<div>").append(a).html())):c?Pa.clone.call(a):a;if(h)for(var k in h)d.data("$"+k+"Controller",h[k].instance);Z.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,e);return d}}function S(a,b,c,d,e,f){function g(a,
53 c,d,e){var f,k,l,s,n,B,C;if(p)for(C=Array(c.length),s=0;s<h.length;s+=3)f=h[s],C[f]=c[f];else C=c;s=0;for(n=h.length;s<n;)if(k=C[h[s++]],c=h[s++],f=h[s++],c){if(c.scope){if(l=a.$new(),Z.$$addScopeInfo(y(k),l),B=c.$$destroyBindings)c.$$destroyBindings=null,l.$on("$destroyed",B)}else l=a;B=c.transcludeOnThisElement?$(a,c.transclude,e):!c.templateOnThisElement&&e?e:!e&&b?$(a,b):null;c(f,l,k,d,B,c)}else f&&f(a,k.childNodes,t,e)}for(var h=[],k,l,s,n,p,B=0;B<a.length;B++){k=new aa;l=ha(a[B],[],k,0===B?
54 d:t,e);(f=l.length?E(l,a[B],k,b,c,null,[],[],f):null)&&f.scope&&Z.$$addScopeClass(k.$$element);k=f&&f.terminal||!(s=a[B].childNodes)||!s.length?null:S(s,f?(f.transcludeOnThisElement||!f.templateOnThisElement)&&f.transclude:b);if(f||k)h.push(B,f,k),n=!0,p=p||f;f=null}return n?g:null}function $(a,b,c){return function(d,e,f,g,h){d||(d=a.$new(!1,h),d.$$transcluded=!0);return b(d,e,{parentBoundTranscludeFn:c,transcludeControllers:f,futureParentElement:g})}}function ha(a,b,c,d,e){var h=c.$attr,k;switch(a.nodeType){case qa:w(b,
55 wa(ta(a)),"E",d,e);for(var l,s,n,p=a.attributes,B=0,C=p&&p.length;B<C;B++){var x=!1,S=!1;l=p[B];k=l.name;s=R(l.value);l=wa(k);if(n=ia.test(l))k=k.replace(Zc,"").substr(8).replace(/_(.)/g,function(a,b){return b.toUpperCase()});var F=l.replace(/(Start|End)$/,"");A(F)&&l===F+"Start"&&(x=k,S=k.substr(0,k.length-5)+"end",k=k.substr(0,k.length-6));l=wa(k.toLowerCase());h[l]=k;if(n||!c.hasOwnProperty(l))c[l]=s,Sc(a,l)&&(c[l]=!0);V(a,b,s,l,n);w(b,l,"A",d,e,x,S)}a=a.className;H(a)&&(a=a.animVal);if(L(a)&&
56 ""!==a)for(;k=g.exec(a);)l=wa(k[2]),w(b,l,"C",d,e)&&(c[l]=R(k[3])),a=a.substr(k.index+k[0].length);break;case Na:if(11===Ua)for(;a.parentNode&&a.nextSibling&&a.nextSibling.nodeType===Na;)a.nodeValue+=a.nextSibling.nodeValue,a.parentNode.removeChild(a.nextSibling);xa(b,a.nodeValue);break;case 8:try{if(k=f.exec(a.nodeValue))l=wa(k[1]),w(b,l,"M",d,e)&&(c[l]=R(k[2]))}catch($){}}b.sort(Aa);return b}function va(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw ea("uterdir",b,c);
57 a.nodeType==qa&&(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 Yc(a,b,c){return function(d,e,f,g,h){e=va(e[0],b,c);return a(d,e,f,g,h)}}function E(a,b,d,e,f,g,h,k,s){function n(a,b,c,d){if(a){c&&(a=Yc(a,c,d));a.require=E.require;a.directiveName=w;if(u===E||E.$$isolateScope)a=X(a,{isolateScope:!0});h.push(a)}if(b){c&&(b=Yc(b,c,d));b.require=E.require;b.directiveName=w;if(u===E||E.$$isolateScope)b=X(b,{isolateScope:!0});k.push(b)}}
58 function B(a,b,c,d){var e;if(L(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;e||(d="$"+b+"Controller",e=g?c.inheritedData(d):c.data(d));if(!e&&!f)throw ea("ctreq",b,a);}else if(G(b))for(e=[],g=0,f=b.length;g<f;g++)e[g]=B(a,b[g],c,d);return e||null}function x(a,b,c,d,e,f){var g=ga(),h;for(h in d){var k=d[h],l={$scope:k===u||k.$$isolateScope?e:f,$element:a,$attrs:b,$transclude:c},s=k.controller;"@"==s&&(s=b[k.name]);l=p(s,
59 l,!0,k.controllerAs);g[k.name]=l;q||a.data("$"+k.name+"Controller",l.instance)}return g}function S(a,c,e,f,g,l){function s(a,b,c){var d;$a(a)||(c=b,b=a,a=t);q&&(d=m);c||(c=q?ja.parent():ja);return g(a,b,d,c,va)}var n,p,C,F,m,ha,ja;b===e?(f=d,ja=d.$$element):(ja=y(e),f=new aa(ja,d));u&&(F=c.$new(!0));g&&(ha=s,ha.$$boundTransclude=g);N&&(m=x(ja,f,ha,N,F,c));u&&(Z.$$addScopeInfo(ja,F,!0,!(D&&(D===u||D===u.$$originalDirective))),Z.$$addScopeClass(ja,!0),F.$$isolateBindings=u.$$isolateBindings,W(c,f,F,
60 F.$$isolateBindings,u,F));if(m){var K=u||$,I;K&&m[K.name]&&(p=K.$$bindings.bindToController,(C=m[K.name])&&C.identifier&&p&&(I=C,l.$$destroyBindings=W(c,f,C.instance,p,K)));for(n in m){C=m[n];var E=C();E!==C.instance&&(C.instance=E,ja.data("$"+n+"Controller",E),C===I&&(l.$$destroyBindings(),l.$$destroyBindings=W(c,f,E,p,K)))}}n=0;for(l=h.length;n<l;n++)p=h[n],Y(p,p.isolateScope?F:c,ja,f,p.require&&B(p.directiveName,p.require,ja,m),ha);var va=c;u&&(u.template||null===u.templateUrl)&&(va=F);a&&a(va,
61 e.childNodes,t,g);for(n=k.length-1;0<=n;n--)p=k[n],Y(p,p.isolateScope?F:c,ja,f,p.require&&B(p.directiveName,p.require,ja,m),ha)}s=s||{};for(var F=-Number.MAX_VALUE,$=s.newScopeDirective,N=s.controllerDirectives,u=s.newIsolateScopeDirective,D=s.templateDirective,m=s.nonTlbTranscludeDirective,K=!1,I=!1,q=s.hasElementTranscludeDirective,ba=d.$$element=y(b),E,w,v,A=e,Aa,xa=0,Ta=a.length;xa<Ta;xa++){E=a[xa];var M=E.$$start,P=E.$$end;M&&(ba=va(b,M,P));v=t;if(F>E.priority)break;if(v=E.scope)E.templateUrl||
62 (H(v)?(O("new/isolated scope",u||$,E,ba),u=E):O("new/isolated scope",u,E,ba)),$=$||E;w=E.name;!E.templateUrl&&E.controller&&(v=E.controller,N=N||ga(),O("'"+w+"' controller",N[w],E,ba),N[w]=E);if(v=E.transclude)K=!0,E.$$tlb||(O("transclusion",m,E,ba),m=E),"element"==v?(q=!0,F=E.priority,v=ba,ba=d.$$element=y(U.createComment(" "+w+": "+d[w]+" ")),b=ba[0],T(f,za.call(v,0),b),A=Z(v,e,F,g&&g.name,{nonTlbTranscludeDirective:m})):(v=y(Vb(b)).contents(),ba.empty(),A=Z(v,e));if(E.template)if(I=!0,O("template",
63 D,E,ba),D=E,v=z(E.template)?E.template(ba,d):E.template,v=fa(v),E.replace){g=E;v=Tb.test(v)?$c(Yb(E.templateNamespace,R(v))):[];b=v[0];if(1!=v.length||b.nodeType!==qa)throw ea("tplrt",w,"");T(f,ba,b);Ta={$attr:{}};v=ha(b,[],Ta);var Q=a.splice(xa+1,a.length-(xa+1));u&&ad(v);a=a.concat(v).concat(Q);J(d,Ta);Ta=a.length}else ba.html(v);if(E.templateUrl)I=!0,O("template",D,E,ba),D=E,E.replace&&(g=E),S=Lf(a.splice(xa,a.length-xa),ba,d,f,K&&A,h,k,{controllerDirectives:N,newScopeDirective:$!==E&&$,newIsolateScopeDirective:u,
64 templateDirective:D,nonTlbTranscludeDirective:m}),Ta=a.length;else if(E.compile)try{Aa=E.compile(ba,d,A),z(Aa)?n(null,Aa,M,P):Aa&&n(Aa.pre,Aa.post,M,P)}catch(Kf){c(Kf,ua(ba))}E.terminal&&(S.terminal=!0,F=Math.max(F,E.priority))}S.scope=$&&!0===$.scope;S.transcludeOnThisElement=K;S.templateOnThisElement=I;S.transclude=A;s.hasElementTranscludeDirective=q;return S}function ad(a){for(var b=0,c=a.length;b<c;b++)a[b]=Ob(a[b],{$$isolateScope:!0})}function w(b,d,f,g,h,k,l){if(d===h)return null;h=null;if(e.hasOwnProperty(d)){var n;
65 d=a.get(d+"Directive");for(var p=0,B=d.length;p<B;p++)try{n=d[p],(g===t||g>n.priority)&&-1!=n.restrict.indexOf(f)&&(k&&(n=Ob(n,{$$start:k,$$end:l})),b.push(n),h=n)}catch(x){c(x)}}return h}function A(b){if(e.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,f=c.length;d<f;d++)if(b=c[d],b.multiElement)return!0;return!1}function J(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;m(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&b[e]!==d&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});m(b,function(b,f){"class"==
66 f?(D(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 Lf(a,b,c,e,f,g,h,k){var l=[],s,n,p=b[0],B=a.shift(),C=Ob(B,{templateUrl:null,transclude:null,replace:null,$$originalDirective:B}),x=z(B.templateUrl)?B.templateUrl(b,c):B.templateUrl,N=B.templateNamespace;b.empty();d(x).then(function(d){var F,u;d=fa(d);if(B.replace){d=Tb.test(d)?$c(Yb(N,R(d))):
67 [];F=d[0];if(1!=d.length||F.nodeType!==qa)throw ea("tplrt",B.name,x);d={$attr:{}};T(e,b,F);var K=ha(F,[],d);H(B.scope)&&ad(K);a=K.concat(a);J(c,d)}else F=p,b.html(d);a.unshift(C);s=E(a,F,c,f,b,B,g,h,k);m(e,function(a,c){a==F&&(e[c]=b[0])});for(n=S(b[0].childNodes,f);l.length;){d=l.shift();u=l.shift();var I=l.shift(),va=l.shift(),K=b[0];if(!d.$$destroyed){if(u!==p){var Z=u.className;k.hasElementTranscludeDirective&&B.replace||(K=Vb(F));T(I,y(u),K);D(y(K),Z)}u=s.transcludeOnThisElement?$(d,s.transclude,
68 va):va;s(n,d,K,e,u,s)}}l=null});return function(a,b,c,d,e){a=e;b.$$destroyed||(l?l.push(b,c,d,a):(s.transcludeOnThisElement&&(a=$(b,s.transclude,e)),s(n,b,c,d,a,s)))}}function Aa(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 O(a,b,c,d){function e(a){return a?" (module: "+a+")":""}if(b)throw ea("multidir",b.name,e(b.$$moduleName),c.name,e(c.$$moduleName),a,ua(d));}function xa(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:function(a){a=
69 a.parent();var b=!!a.length;b&&Z.$$addBindingClass(a);return function(a,c){var e=c.parent();b||Z.$$addBindingClass(e);Z.$$addBindingInfo(e,d.expressions);a.$watch(d,function(a){c[0].nodeValue=a})}}})}function Yb(a,b){a=M(a||"html");switch(a){case "svg":case "math":var c=U.createElement("div");c.innerHTML="<"+a+">"+b+"</"+a+">";return c.childNodes[0].childNodes;default:return b}}function Q(a,b){if("srcdoc"==b)return I.HTML;var c=ta(a);if("xlinkHref"==b||"form"==c&&"action"==b||"img"!=c&&("src"==b||
70 "ngSrc"==b))return I.RESOURCE_URL}function V(a,c,d,e,f){var g=Q(a,e);f=h[e]||f;var l=b(d,!0,g,f);if(l){if("multiple"===e&&"select"===ta(a))throw ea("selmulti",ua(a));c.push({priority:100,compile:function(){return{pre:function(a,c,h){c=h.$$observers||(h.$$observers={});if(k.test(e))throw ea("nodomevents");var s=h[e];s!==d&&(l=s&&b(s,!0,g,f),d=s);l&&(h[e]=l(a),(c[e]||(c[e]=[])).$$inter=!0,(h.$$observers&&h.$$observers[e].$$scope||a).$watch(l,function(a,b){"class"===e&&a!=b?h.$updateClass(a,b):h.$set(e,
71 a)}))}}}})}}function T(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g<h;g++)if(a[g]==d){a[g++]=c;h=g+e-1;for(var k=a.length;g<k;g++,h++)h<k?a[g]=a[h]:delete a[g];a.length-=e-1;a.context===d&&(a.context=c);break}f&&f.replaceChild(c,d);a=U.createDocumentFragment();a.appendChild(d);y.hasData(d)&&(y(c).data(y(d).data()),la?(Rb=!0,la.cleanData([d])):delete y.cache[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 X(a,
72 b){return P(function(){return a.apply(null,arguments)},a,b)}function Y(a,b,d,e,f,g){try{a(b,d,e,f,g)}catch(h){c(h,ua(d))}}function W(a,c,d,e,f,g){var h;m(e,function(e,g){var k=e.attrName,l=e.optional,s=e.mode,n,p,B,C;Xa.call(c,k)||(c[k]=t);switch(s){case "@":c[k]||l||(d[g]=t);c.$observe(k,function(a){d[g]=a});c.$$observers[k].$$scope=a;c[k]&&(d[g]=b(c[k])(a));break;case "=":if(l&&!c[k])break;p=u(c[k]);C=p.literal?ka:function(a,b){return a===b||a!==a&&b!==b};B=p.assign||function(){n=d[g]=p(a);throw ea("nonassign",
73 c[k],f.name);};n=d[g]=p(a);l=function(b){C(b,d[g])||(C(b,n)?B(a,b=d[g]):d[g]=b);return n=b};l.$stateful=!0;l=e.collection?a.$watchCollection(c[k],l):a.$watch(u(c[k],l),null,p.literal);h=h||[];h.push(l);break;case "&":p=u(c[k]);if(p===v&&l)break;d[g]=function(b){return p(a,b)}}});e=h?function(){for(var a=0,b=h.length;a<b;++a)h[a]()}:v;return g&&e!==v?(g.$on("$destroy",e),v):e}var aa=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=
74 a};aa.prototype={$normalize:wa,$addClass:function(a){a&&0<a.length&&B.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&B.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=bd(a,b);c&&c.length&&B.addClass(this.$$element,c);(c=bd(b,a))&&c.length&&B.removeClass(this.$$element,c)},$set:function(a,b,d,e){var f=this.$$element[0],g=Sc(f,a),h=Ff(f,a),f=a;g?(this.$$element.prop(a,b),e=g):h&&(this[h]=b,f=h);this[a]=b;e?this.$attr[a]=e:(e=this.$attr[a])||(this.$attr[a]=e=Bc(a,
75 "-"));g=ta(this.$$element);if("a"===g&&"href"===a||"img"===g&&"src"===a)this[a]=b=N(b,"src"===a);else if("img"===g&&"srcset"===a){for(var g="",h=R(b),k=/(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/,k=/\s/.test(h)?k:/(,)/,h=h.split(k),k=Math.floor(h.length/2),l=0;l<k;l++)var s=2*l,g=g+N(R(h[s]),!0),g=g+(" "+R(h[s+1]));h=R(h[2*l]).split(/\s/);g+=N(R(h[0]),!0);2===h.length&&(g+=" "+R(h[1]));this[a]=b=g}!1!==d&&(null===b||b===t?this.$$element.removeAttr(e):this.$$element.attr(e,b));(a=this.$$observers)&&m(a[f],
76 function(a){try{a(b)}catch(d){c(d)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers=ga()),e=d[a]||(d[a]=[]);e.push(b);K.$evalAsync(function(){!e.$$inter&&c.hasOwnProperty(a)&&b(c[a])});return function(){bb(e,b)}}};var ca=b.startSymbol(),da=b.endSymbol(),fa="{{"==ca||"}}"==da?Ya:function(a){return a.replace(/\{\{/g,ca).replace(/}}/g,da)},ia=/^ngAttr[A-Z]/;Z.$$addBindingInfo=n?function(a,b){var c=a.data("$binding")||[];G(b)?c=c.concat(b):c.push(b);a.data("$binding",c)}:v;Z.$$addBindingClass=
77 n?function(a){D(a,"ng-binding")}:v;Z.$$addScopeInfo=n?function(a,b,c,d){a.data(c?d?"$isolateScopeNoTemplate":"$isolateScope":"$scope",b)}:v;Z.$$addScopeClass=n?function(a,b){D(a,b?"ng-isolate-scope":"ng-scope")}:v;return Z}]}function wa(b){return hb(b.replace(Zc,""))}function bd(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 $c(b){b=y(b);var a=b.length;if(1>=a)return b;for(;a--;)8===
78 b[a].nodeType&&Mf.call(b,a,1);return b}function Xe(){var b={},a=!1;this.register=function(a,d){Ra(a,"controller");H(a)?P(b,a):b[a]=d};this.allowGlobals=function(){a=!0};this.$get=["$injector","$window",function(c,d){function e(a,b,c,d){if(!a||!H(a.$scope))throw J("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,g,h,l){var k,n,r;h=!0===h;l&&L(l)&&(r=l);if(L(f)){l=f.match(Xc);if(!l)throw Nf("ctrlfmt",f);n=l[1];r=r||l[3];f=b.hasOwnProperty(n)?b[n]:Dc(g.$scope,n,!0)||(a?Dc(d,n,!0):t);Qa(f,
79 n,!0)}if(h)return h=(G(f)?f[f.length-1]:f).prototype,k=Object.create(h||null),r&&e(g,r,k,n||f.name),P(function(){var a=c.invoke(f,k,g,n);a!==k&&(H(a)||z(a))&&(k=a,r&&e(g,r,k,n||f.name));return k},{instance:k,identifier:r});k=c.instantiate(f,g,n);r&&e(g,r,k,n||f.name);return k}}]}function Ye(){this.$get=["$window",function(b){return y(b.document)}]}function Ze(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function Zb(b){return H(b)?aa(b)?b.toISOString():db(b):b}
80 function cf(){this.$get=function(){return function(b){if(!b)return"";var a=[];oc(b,function(b,d){null===b||A(b)||(G(b)?m(b,function(b,c){a.push(ma(d)+"="+ma(Zb(b)))}):a.push(ma(d)+"="+ma(Zb(b))))});return a.join("&")}}}function df(){this.$get=function(){return function(b){function a(b,e,f){null===b||A(b)||(G(b)?m(b,function(b){a(b,e+"[]")}):H(b)&&!aa(b)?oc(b,function(b,c){a(b,e+(f?"":"[")+c+(f?"":"]"))}):c.push(ma(e)+"="+ma(Zb(b))))}if(!b)return"";var c=[];a(b,"",!0);return c.join("&")}}}function $b(b,
81 a){if(L(b)){var c=b.replace(Of,"").trim();if(c){var d=a("Content-Type");(d=d&&0===d.indexOf(cd))||(d=(d=c.match(Pf))&&Qf[d[0]].test(c));d&&(b=wc(c))}}return b}function dd(b){var a=ga(),c;L(b)?m(b.split("\n"),function(b){c=b.indexOf(":");var e=M(R(b.substr(0,c)));b=R(b.substr(c+1));e&&(a[e]=a[e]?a[e]+", "+b:b)}):H(b)&&m(b,function(b,c){var f=M(c),g=R(b);f&&(a[f]=a[f]?a[f]+", "+g:g)});return a}function ed(b){var a;return function(c){a||(a=dd(b));return c?(c=a[M(c)],void 0===c&&(c=null),c):a}}function fd(b,
82 a,c,d){if(z(d))return d(b,a,c);m(d,function(d){b=d(b,a,c)});return b}function bf(){var b=this.defaults={transformResponse:[$b],transformRequest:[function(a){return H(a)&&"[object File]"!==sa.call(a)&&"[object Blob]"!==sa.call(a)&&"[object FormData]"!==sa.call(a)?db(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ia(ac),put:ia(ac),patch:ia(ac)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer"},a=!1;this.useApplyAsync=function(b){return w(b)?
83 (a=!!b,this):a};var c=this.interceptors=[];this.$get=["$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector",function(d,e,f,g,h,l){function k(a){function c(a){var b=P({},a);b.data=a.data?fd(a.data,a.headers,a.status,e.transformResponse):a.data;a=a.status;return 200<=a&&300>a?b:h.reject(b)}function d(a,b){var c,e={};m(a,function(a,d){z(a)?(c=a(b),null!=c&&(e[d]=c)):e[d]=a});return e}if(!ca.isObject(a))throw J("$http")("badreq",a);var e=P({method:"get",transformRequest:b.transformRequest,
84 transformResponse:b.transformResponse,paramSerializer:b.paramSerializer},a);e.headers=function(a){var c=b.headers,e=P({},a.headers),f,g,h,c=P({},c.common,c[M(a.method)]);a:for(f in c){g=M(f);for(h in e)if(M(h)===g)continue a;e[f]=c[f]}return d(e,ia(a))}(a);e.method=rb(e.method);e.paramSerializer=L(e.paramSerializer)?l.get(e.paramSerializer):e.paramSerializer;var f=[function(a){var d=a.headers,e=fd(a.data,ed(d),t,a.transformRequest);A(e)&&m(d,function(a,b){"content-type"===M(b)&&delete d[b]});A(a.withCredentials)&&
85 !A(b.withCredentials)&&(a.withCredentials=b.withCredentials);return n(a,e).then(c,c)},t],g=h.when(e);for(m(x,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 k=f.shift(),g=g.then(a,k)}g.success=function(a){Qa(a,"fn");g.then(function(b){a(b.data,b.status,b.headers,e)});return g};g.error=function(a){Qa(a,"fn");g.then(null,function(b){a(b.data,b.status,b.headers,e)});return g};return g}
86 function n(c,f){function l(b,c,d,e){function f(){n(c,b,d,e)}N&&(200<=b&&300>b?N.put(S,[b,c,dd(d),e]):N.remove(S));a?g.$applyAsync(f):(f(),g.$$phase||g.$apply())}function n(a,b,d,e){b=Math.max(b,0);(200<=b&&300>b?I.resolve:I.reject)({data:a,status:b,headers:ed(d),config:c,statusText:e})}function x(a){n(a.data,a.status,ia(a.headers()),a.statusText)}function m(){var a=k.pendingRequests.indexOf(c);-1!==a&&k.pendingRequests.splice(a,1)}var I=h.defer(),B=I.promise,N,D,q=c.headers,S=r(c.url,c.paramSerializer(c.params));
87 k.pendingRequests.push(c);B.then(m,m);!c.cache&&!b.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(N=H(c.cache)?c.cache:H(b.cache)?b.cache:s);N&&(D=N.get(S),w(D)?D&&z(D.then)?D.then(x,x):G(D)?n(D[1],D[0],ia(D[2]),D[3]):n(D,200,{},"OK"):N.put(S,B));A(D)&&((D=gd(c.url)?e()[c.xsrfCookieName||b.xsrfCookieName]:t)&&(q[c.xsrfHeaderName||b.xsrfHeaderName]=D),d(c.method,S,f,l,q,c.timeout,c.withCredentials,c.responseType));return B}function r(a,b){0<b.length&&(a+=(-1==a.indexOf("?")?"?":"&")+b);
88 return a}var s=f("$http");b.paramSerializer=L(b.paramSerializer)?l.get(b.paramSerializer):b.paramSerializer;var x=[];m(c,function(a){x.unshift(L(a)?l.get(a):l.invoke(a))});k.pendingRequests=[];(function(a){m(arguments,function(a){k[a]=function(b,c){return k(P({},c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){m(arguments,function(a){k[a]=function(b,c,d){return k(P({},d||{},{method:a,url:b,data:c}))}})})("post","put","patch");k.defaults=b;return k}]}function Rf(){return new O.XMLHttpRequest}
89 function ef(){this.$get=["$browser","$window","$document",function(b,a,c){return Sf(b,Rf,b.defer,a.angular.callbacks,c[0])}]}function Sf(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,x="unknown";a&&("load"!==a.type||d[b].called||(a={type:"error"}),x=a.type,g="error"===a.type?404:200);c&&c(g,x)};f.addEventListener("load",
90 n,!1);f.addEventListener("error",n,!1);e.body.appendChild(f);return n}return function(e,h,l,k,n,r,s,x){function C(){p&&p();K&&K.abort()}function F(a,d,e,f,g){I!==t&&c.cancel(I);p=K=null;a(d,e,f,g);b.$$completeOutstandingRequest(v)}b.$$incOutstandingRequestCount();h=h||b.url();if("jsonp"==M(e)){var u="_"+(d.counter++).toString(36);d[u]=function(a){d[u].data=a;d[u].called=!0};var p=f(h.replace("JSON_CALLBACK","angular.callbacks."+u),u,function(a,b){F(k,a,d[u].data,"",b);d[u]=v})}else{var K=a();K.open(e,
91 h,!0);m(n,function(a,b){w(a)&&K.setRequestHeader(b,a)});K.onload=function(){var a=K.statusText||"",b="response"in K?K.response:K.responseText,c=1223===K.status?204:K.status;0===c&&(c=b?200:"file"==Ba(h).protocol?404:0);F(k,c,b,K.getAllResponseHeaders(),a)};e=function(){F(k,-1,null,null,"")};K.onerror=e;K.onabort=e;s&&(K.withCredentials=!0);if(x)try{K.responseType=x}catch(q){if("json"!==x)throw q;}K.send(l)}if(0<r)var I=c(C,r);else r&&z(r.then)&&r.then(C)}}function $e(){var b="{{",a="}}";this.startSymbol=
92 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(c){return c.replace(n,b).replace(r,a)}function h(f,h,n,r){function u(a){try{var b=a;a=n?e.getTrusted(n,b):e.valueOf(b);var c;if(r&&!w(a))c=a;else if(null==a)c="";else{switch(typeof a){case "string":break;case "number":a=""+a;break;default:a=db(a)}c=a}return c}catch(g){d(Ka.interr(f,g))}}r=!!r;for(var p,m,q=0,I=
93 [],B=[],N=f.length,D=[],t=[];q<N;)if(-1!=(p=f.indexOf(b,q))&&-1!=(m=f.indexOf(a,p+l)))q!==p&&D.push(g(f.substring(q,p))),q=f.substring(p+l,m),I.push(q),B.push(c(q,u)),q=m+k,t.push(D.length),D.push("");else{q!==N&&D.push(g(f.substring(q)));break}n&&1<D.length&&Ka.throwNoconcat(f);if(!h||I.length){var S=function(a){for(var b=0,c=I.length;b<c;b++){if(r&&A(a[b]))return;D[t[b]]=a[b]}return D.join("")};return P(function(a){var b=0,c=I.length,e=Array(c);try{for(;b<c;b++)e[b]=B[b](a);return S(e)}catch(g){d(Ka.interr(f,
94 g))}},{exp:f,expressions:I,$$watchDelegate:function(a,b){var c;return a.$watchGroup(B,function(d,e){var f=S(d);z(b)&&b.call(this,f,d!==e?c:f,a);c=f})}})}}var l=b.length,k=a.length,n=new RegExp(b.replace(/./g,f),"g"),r=new RegExp(a.replace(/./g,f),"g");h.startSymbol=function(){return b};h.endSymbol=function(){return a};return h}]}function af(){this.$get=["$rootScope","$window","$q","$$q",function(b,a,c,d){function e(e,h,l,k){var n=4<arguments.length,r=n?za.call(arguments,4):[],s=a.setInterval,x=a.clearInterval,
95 C=0,F=w(k)&&!k,u=(F?d:c).defer(),p=u.promise;l=w(l)?l:0;p.then(null,null,n?function(){e.apply(null,r)}:e);p.$$intervalId=s(function(){u.notify(C++);0<l&&C>=l&&(u.resolve(C),x(p.$$intervalId),delete f[p.$$intervalId]);F||b.$apply()},h);f[p.$$intervalId]=u;return p}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 ge(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",
96 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(" "),
97 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",ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"]},pluralCat:function(b){return 1===b?"one":"other"}}}}function bc(b){b=b.split("/");for(var a=b.length;a--;)b[a]=ob(b[a]);return b.join("/")}function hd(b,a){var c=Ba(b);a.$$protocol=c.protocol;
98 a.$$host=c.hostname;a.$$port=W(c.port)||Tf[c.protocol]||null}function id(b,a){var c="/"!==b.charAt(0);c&&(b="/"+b);var d=Ba(b);a.$$path=decodeURIComponent(c&&"/"===d.pathname.charAt(0)?d.pathname.substring(1):d.pathname);a.$$search=zc(d.search);a.$$hash=decodeURIComponent(d.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function ya(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Ja(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Bb(b){return b.replace(/(#.+)|#$/,
99 "$1")}function cc(b){return b.substr(0,Ja(b).lastIndexOf("/")+1)}function dc(b,a){this.$$html5=!0;a=a||"";var c=cc(b);hd(b,this);this.$$parse=function(a){var b=ya(c,a);if(!L(b))throw Cb("ipthprfx",a,c);id(b,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Qb(this.$$search),b=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(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)),
100 !0;var f,g;(f=ya(b,d))!==t?(g=f,g=(f=ya(a,f))!==t?c+(ya("/",f)||f):b+g):(f=ya(c,d))!==t?g=c+f:c==d+"/"&&(g=c);g&&this.$$parse(g);return!!g}}function ec(b,a){var c=cc(b);hd(b,this);this.$$parse=function(d){var e=ya(b,d)||ya(c,d),f;A(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",A(e)&&(b=d,this.replace())):(f=ya(a,e),A(f)&&(f=e));id(f,this);d=this.$$path;var e=b,g=/^\/[A-Z]:(\/.*)/;0===f.indexOf(e)&&(f=f.replace(e,""));g.exec(f)||(d=(f=g.exec(d))?f[1]:d);this.$$path=d;this.$$compose()};this.$$compose=
101 function(){var c=Qb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+(this.$$url?a+this.$$url:"")};this.$$parseLinkUrl=function(a,c){return Ja(b)==Ja(a)?(this.$$parse(a),!0):!1}}function jd(b,a){this.$$html5=!0;ec.apply(this,arguments);var c=cc(b);this.$$parseLinkUrl=function(d,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;b==Ja(d)?f=d:(g=ya(c,d))?f=b+a+g:c===d+"/"&&(f=c);f&&this.$$parse(f);return!!f};this.$$compose=function(){var c=
102 Qb(this.$$search),e=this.$$hash?"#"+ob(this.$$hash):"";this.$$url=bc(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=b+a+this.$$url}}function Db(b){return function(){return this[b]}}function kd(b,a){return function(c){if(A(c))return this[b];this[b]=a(c);this.$$compose();return this}}function ff(){var b="",a={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(a){return w(a)?(b=a,this):b};this.html5Mode=function(b){return ab(b)?(a.enabled=b,this):H(b)?(ab(b.enabled)&&(a.enabled=b.enabled),
103 ab(b.requireBase)&&(a.requireBase=b.requireBase),ab(b.rewriteLinks)&&(a.rewriteLinks=b.rewriteLinks),this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(c,d,e,f,g){function h(a,b,c){var e=k.url(),f=k.$$state;try{d.url(a,b,c),k.$$state=d.state()}catch(g){throw k.url(e),k.$$state=f,g;}}function l(a,b){c.$broadcast("$locationChangeSuccess",k.absUrl(),a,k.$$state,b)}var k,n;n=d.baseHref();var r=d.url(),s;if(a.enabled){if(!n&&a.requireBase)throw Cb("nobase");s=r.substring(0,
104 r.indexOf("/",r.indexOf("//")+2))+(n||"/");n=e.history?dc:jd}else s=Ja(r),n=ec;k=new n(s,"#"+b);k.$$parseLinkUrl(r,r);k.$$state=d.state();var x=/^\s*(javascript|mailto):/i;f.on("click",function(b){if(a.rewriteLinks&&!b.ctrlKey&&!b.metaKey&&!b.shiftKey&&2!=b.which&&2!=b.button){for(var e=y(b.target);"a"!==ta(e[0]);)if(e[0]===f[0]||!(e=e.parent())[0])return;var h=e.prop("href"),l=e.attr("href")||e.attr("xlink:href");H(h)&&"[object SVGAnimatedString]"===h.toString()&&(h=Ba(h.animVal).href);x.test(h)||
105 !h||e.attr("target")||b.isDefaultPrevented()||!k.$$parseLinkUrl(h,l)||(b.preventDefault(),k.absUrl()!=d.url()&&(c.$apply(),g.angular["ff-684208-preventDefault"]=!0))}});Bb(k.absUrl())!=Bb(r)&&d.url(k.absUrl(),!0);var C=!0;d.onUrlChange(function(a,b){c.$evalAsync(function(){var d=k.absUrl(),e=k.$$state,f;k.$$parse(a);k.$$state=b;f=c.$broadcast("$locationChangeStart",a,d,b,e).defaultPrevented;k.absUrl()===a&&(f?(k.$$parse(d),k.$$state=e,h(d,!1,e)):(C=!1,l(d,e)))});c.$$phase||c.$digest()});c.$watch(function(){var a=
106 Bb(d.url()),b=Bb(k.absUrl()),f=d.state(),g=k.$$replace,n=a!==b||k.$$html5&&e.history&&f!==k.$$state;if(C||n)C=!1,c.$evalAsync(function(){var b=k.absUrl(),d=c.$broadcast("$locationChangeStart",b,a,k.$$state,f).defaultPrevented;k.absUrl()===b&&(d?(k.$$parse(a),k.$$state=f):(n&&h(b,g,f===k.$$state?null:k.$$state),l(a,f)))});k.$$replace=!1});return k}]}function gf(){var b=!0,a=this;this.debugEnabled=function(a){return w(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&
107 (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||v;a=!1;try{a=!!e.apply}catch(l){}return a?function(){var a=[];m(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)}}()}}]}
108 function Ca(b,a){if("__defineGetter__"===b||"__defineSetter__"===b||"__lookupGetter__"===b||"__lookupSetter__"===b||"__proto__"===b)throw da("isecfld",a);return b}function oa(b,a){if(b){if(b.constructor===b)throw da("isecfn",a);if(b.window===b)throw da("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw da("isecdom",a);if(b===Object)throw da("isecobj",a);}return b}function ld(b,a){if(b){if(b.constructor===b)throw da("isecfn",a);if(b===Uf||b===Vf||b===Wf)throw da("isecff",a);
109 }}function Xf(b,a){return"undefined"!==typeof b?b:a}function md(b,a){return"undefined"===typeof b?a:"undefined"===typeof a?b:b+a}function T(b,a){var c,d;switch(b.type){case q.Program:c=!0;m(b.body,function(b){T(b.expression,a);c=c&&b.expression.constant});b.constant=c;break;case q.Literal:b.constant=!0;b.toWatch=[];break;case q.UnaryExpression:T(b.argument,a);b.constant=b.argument.constant;b.toWatch=b.argument.toWatch;break;case q.BinaryExpression:T(b.left,a);T(b.right,a);b.constant=b.left.constant&&
110 b.right.constant;b.toWatch=b.left.toWatch.concat(b.right.toWatch);break;case q.LogicalExpression:T(b.left,a);T(b.right,a);b.constant=b.left.constant&&b.right.constant;b.toWatch=b.constant?[]:[b];break;case q.ConditionalExpression:T(b.test,a);T(b.alternate,a);T(b.consequent,a);b.constant=b.test.constant&&b.alternate.constant&&b.consequent.constant;b.toWatch=b.constant?[]:[b];break;case q.Identifier:b.constant=!1;b.toWatch=[b];break;case q.MemberExpression:T(b.object,a);b.computed&&T(b.property,a);
111 b.constant=b.object.constant&&(!b.computed||b.property.constant);b.toWatch=[b];break;case q.CallExpression:c=b.filter?!a(b.callee.name).$stateful:!1;d=[];m(b.arguments,function(b){T(b,a);c=c&&b.constant;b.constant||d.push.apply(d,b.toWatch)});b.constant=c;b.toWatch=b.filter&&!a(b.callee.name).$stateful?d:[b];break;case q.AssignmentExpression:T(b.left,a);T(b.right,a);b.constant=b.left.constant&&b.right.constant;b.toWatch=[b];break;case q.ArrayExpression:c=!0;d=[];m(b.elements,function(b){T(b,a);c=
112 c&&b.constant;b.constant||d.push.apply(d,b.toWatch)});b.constant=c;b.toWatch=d;break;case q.ObjectExpression:c=!0;d=[];m(b.properties,function(b){T(b.value,a);c=c&&b.value.constant;b.value.constant||d.push.apply(d,b.value.toWatch)});b.constant=c;b.toWatch=d;break;case q.ThisExpression:b.constant=!1,b.toWatch=[]}}function nd(b){if(1==b.length){b=b[0].expression;var a=b.toWatch;return 1!==a.length?a:a[0]!==b?a:t}}function od(b){return b.type===q.Identifier||b.type===q.MemberExpression}function pd(b){if(1===
113 b.body.length&&od(b.body[0].expression))return{type:q.AssignmentExpression,left:b.body[0].expression,right:{type:q.NGValueParameter},operator:"="}}function qd(b){return 0===b.body.length||1===b.body.length&&(b.body[0].expression.type===q.Literal||b.body[0].expression.type===q.ArrayExpression||b.body[0].expression.type===q.ObjectExpression)}function rd(b,a){this.astBuilder=b;this.$filter=a}function sd(b,a){this.astBuilder=b;this.$filter=a}function Eb(b,a,c,d){oa(b,d);a=a.split(".");for(var e,f=0;1<
114 a.length;f++){e=Ca(a.shift(),d);var g=oa(b[e],d);g||(g={},b[e]=g);b=g}e=Ca(a.shift(),d);oa(b[e],d);return b[e]=c}function Fb(b){return"constructor"==b}function fc(b){return z(b.valueOf)?b.valueOf():Yf.call(b)}function hf(){var b=ga(),a=ga();this.$get=["$filter","$sniffer",function(c,d){function e(a,b){return null==a||null==b?a===b:"object"===typeof a&&(a=fc(a),"object"===typeof a)?!1:a===b||a!==a&&b!==b}function f(a,b,c,d,f){var g=d.inputs,h;if(1===g.length){var k=e,g=g[0];return a.$watch(function(a){var b=
115 g(a);e(b,k)||(h=d(a,t,t,[b]),k=b&&fc(b));return h},b,c,f)}for(var l=[],n=[],r=0,m=g.length;r<m;r++)l[r]=e,n[r]=null;return a.$watch(function(a){for(var b=!1,c=0,f=g.length;c<f;c++){var k=g[c](a);if(b||(b=!e(k,l[c])))n[c]=k,l[c]=k&&fc(k)}b&&(h=d(a,t,t,n));return h},b,c,f)}function g(a,b,c,d){var e,f;return e=a.$watch(function(a){return d(a)},function(a,c,d){f=a;z(b)&&b.apply(this,arguments);w(a)&&d.$$postDigest(function(){w(f)&&e()})},c)}function h(a,b,c,d){function e(a){var b=!0;m(a,function(a){w(a)||
116 (b=!1)});return b}var f,g;return f=a.$watch(function(a){return d(a)},function(a,c,d){g=a;z(b)&&b.call(this,a,c,d);e(a)&&d.$$postDigest(function(){e(g)&&f()})},c)}function l(a,b,c,d){var e;return e=a.$watch(function(a){return d(a)},function(a,c,d){z(b)&&b.apply(this,arguments);e()},c)}function k(a,b){if(!b)return a;var c=a.$$watchDelegate,c=c!==h&&c!==g?function(c,d,e,f){e=a(c,d,e,f);return b(e,c,d)}:function(c,d,e,f){e=a(c,d,e,f);c=b(e,c,d);return w(e)?c:e};a.$$watchDelegate&&a.$$watchDelegate!==
117 f?c.$$watchDelegate=a.$$watchDelegate:b.$stateful||(c.$$watchDelegate=f,c.inputs=a.inputs?a.inputs:[a]);return c}var n={csp:d.csp,expensiveChecks:!1},r={csp:d.csp,expensiveChecks:!0};return function(d,e,C){var m,u,p;switch(typeof d){case "string":p=d=d.trim();var q=C?a:b;m=q[p];m||(":"===d.charAt(0)&&":"===d.charAt(1)&&(u=!0,d=d.substring(2)),C=C?r:n,m=new gc(C),m=(new hc(m,c,C)).parse(d),m.constant?m.$$watchDelegate=l:u?m.$$watchDelegate=m.literal?h:g:m.inputs&&(m.$$watchDelegate=f),q[p]=m);return k(m,
118 e);case "function":return k(d,e);default:return v}}}]}function kf(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return td(function(a){b.$evalAsync(a)},a)}]}function lf(){this.$get=["$browser","$exceptionHandler",function(b,a){return td(function(a){b.defer(a)},a)}]}function td(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&&
119 c.pending&&(c.processScheduled=!0,b(function(){var b,d,e;e=c.pending;c.processScheduled=!1;c.pending=t;for(var f=0,g=e.length;f<g;++f){d=e[f][0];b=e[f][c.status];try{z(b)?d.resolve(b(c.value)):1===c.status?d.resolve(c.value):d.reject(c.value)}catch(h){d.reject(h),a(h)}}}))}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=J("$q",TypeError);d.prototype={then:function(a,b,c){var d=new g;this.$$state.pending=this.$$state.pending||
120 [];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(H(b)||z(b))d=b&&b.then;z(d)?(this.promise.$$state.status=
121 -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(g){e[1](g),a(g)}},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,g=d.length;f<g;f++){e=d[f][0];b=d[f][3];try{e.notify(z(b)?
122 b(c):c)}catch(h){a(h)}}})}};var l=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{z(c)&&(d=c())}catch(e){return l(e,!1)}return d&&z(d.then)?d.then(function(){return l(a,b)},function(a){return l(a,!1)}):l(a,b)},n=function(a,b,c,d){var e=new g;e.resolve(a);return e.promise.then(b,c,d)},r=function x(a){if(!z(a))throw h("norslvr",a);if(!(this instanceof x))return new x(a);var b=new g;a(function(a){b.resolve(a)},function(a){b.reject(a)});return b.promise};
123 r.defer=function(){return new g};r.reject=function(a){var b=new g;b.reject(a);return b.promise};r.when=n;r.resolve=n;r.all=function(a){var b=new g,c=0,d=G(a)?[]:{};m(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 r}function uf(){this.$get=["$window","$timeout",function(b,a){function c(){for(var a=0;a<n.length;a++){var b=n[a];b&&(n[a]=null,b())}k=n.length=0}function d(a){var b=
124 n.length;k++;n.push(a);0===b&&(l=h(c));return function(){0<=b&&(b=n[b]=null,0===--k&&l&&(l(),l=null,n.length=0))}}var e=b.requestAnimationFrame||b.webkitRequestAnimationFrame,f=b.cancelAnimationFrame||b.webkitCancelAnimationFrame||b.webkitCancelRequestAnimationFrame,g=!!e,h=g?function(a){var b=e(a);return function(){f(b)}}:function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}};d.supported=g;var l,k=0,n=[];return d}]}function jf(){function b(a){function b(){this.$$watchers=this.$$nextSibling=
125 this.$$childHead=this.$$childTail=null;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$id=++nb;this.$$ChildScope=null}b.prototype=a;return b}var a=10,c=J("$rootScope"),d=null,e=null;this.digestTtl=function(b){arguments.length&&(a=b);return a};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(f,g,h,l){function k(a){a.currentScope.$$destroyed=!0}function n(){this.$id=++nb;this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=
126 this.$$childTail=null;this.$root=this;this.$$destroyed=!1;this.$$listeners={};this.$$listenerCount={};this.$$watchersCount=0;this.$$isolateBindings=null}function r(a){if(p.$$phase)throw c("inprog",p.$$phase);p.$$phase=a}function s(a,b){do a.$$watchersCount+=b;while(a=a.$parent)}function x(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function q(){}function F(){for(;I.length;)try{I.shift()()}catch(a){g(a)}e=null}function u(){null===e&&(e=
127 l.defer(function(){p.$apply(F)}))}n.prototype={constructor:n,$new:function(a,c){var d;c=c||this;a?(d=new n,d.$root=this.$root):(this.$$ChildScope||(this.$$ChildScope=b(this)),d=new this.$$ChildScope);d.$parent=c;d.$$prevSibling=c.$$childTail;c.$$childHead?(c.$$childTail.$$nextSibling=d,c.$$childTail=d):c.$$childHead=c.$$childTail=d;(a||c!=this)&&d.$on("$destroy",k);return d},$watch:function(a,b,c,e){var f=h(a);if(f.$$watchDelegate)return f.$$watchDelegate(this,b,c,f,a);var g=this,k=g.$$watchers,l=
128 {fn:b,last:q,get:f,exp:e||a,eq:!!c};d=null;z(b)||(l.fn=v);k||(k=g.$$watchers=[]);k.unshift(l);s(this,1);return function(){0<=bb(k,l)&&s(g,-1);d=null}},$watchGroup:function(a,b){function c(){h=!1;k?(k=!1,b(e,e,g)):b(e,d,g)}var d=Array(a.length),e=Array(a.length),f=[],g=this,h=!1,k=!0;if(!a.length){var l=!0;g.$evalAsync(function(){l&&b(e,e,g)});return function(){l=!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)});m(a,function(a,b){var k=g.$watch(a,function(a,
129 f){e[b]=a;d[b]=f;h||(h=!0,g.$evalAsync(c))});f.push(k)});return function(){for(;f.length;)f.shift()()}},$watchCollection:function(a,b){function c(a){e=a;var b,d,g,h;if(!A(e)){if(H(e))if(Ea(e))for(f!==r&&(f=r,m=f.length=0,l++),a=e.length,m!==a&&(l++,f.length=m=a),b=0;b<a;b++)h=f[b],g=e[b],d=h!==h&&g!==g,d||h===g||(l++,f[b]=g);else{f!==s&&(f=s={},m=0,l++);a=0;for(b in e)e.hasOwnProperty(b)&&(a++,g=e[b],h=f[b],b in f?(d=h!==h&&g!==g,d||h===g||(l++,f[b]=g)):(m++,f[b]=g,l++));if(m>a)for(b in l++,f)e.hasOwnProperty(b)||
130 (m--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,g,k=1<b.length,l=0,n=h(a,c),r=[],s={},p=!0,m=0;return this.$watch(n,function(){p?(p=!1,b(e,e,d)):b(e,g,d);if(k)if(H(e))if(Ea(e)){g=Array(e.length);for(var a=0;a<e.length;a++)g[a]=e[a]}else for(a in g={},e)Xa.call(e,a)&&(g[a]=e[a]);else g=e})},$digest:function(){var b,f,h,k,n,s,m=a,x,u=[],E,I;r("$digest");l.$$checkUrlChange();this===p&&null!==e&&(l.defer.cancel(e),F());d=null;do{s=!1;for(x=this;t.length;){try{I=t.shift(),
131 I.scope.$eval(I.expression,I.locals)}catch(v){g(v)}d=null}a:do{if(k=x.$$watchers)for(n=k.length;n--;)try{if(b=k[n])if((f=b.get(x))!==(h=b.last)&&!(b.eq?ka(f,h):"number"===typeof f&&"number"===typeof h&&isNaN(f)&&isNaN(h)))s=!0,d=b,b.last=b.eq?fa(f,null):f,b.fn(f,h===q?f:h,x),5>m&&(E=4-m,u[E]||(u[E]=[]),u[E].push({msg:z(b.exp)?"fn: "+(b.exp.name||b.exp.toString()):b.exp,newVal:f,oldVal:h}));else if(b===d){s=!1;break a}}catch(A){g(A)}if(!(k=x.$$watchersCount&&x.$$childHead||x!==this&&x.$$nextSibling))for(;x!==
132 this&&!(k=x.$$nextSibling);)x=x.$parent}while(x=k);if((s||t.length)&&!m--)throw p.$$phase=null,c("infdig",a,u);}while(s||t.length);for(p.$$phase=null;w.length;)try{w.shift()()}catch(y){g(y)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this===p&&l.$$applicationDestroyed();s(this,-this.$$watchersCount);for(var b in this.$$listenerCount)x(this,this.$$listenerCount[b],b);a&&a.$$childHead==this&&(a.$$childHead=this.$$nextSibling);a&&a.$$childTail==
133 this&&(a.$$childTail=this.$$prevSibling);this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling);this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling);this.$destroy=this.$digest=this.$apply=this.$evalAsync=this.$applyAsync=v;this.$on=this.$watch=this.$watchGroup=function(){return v};this.$$listeners={};this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=this.$$watchers=null}},$eval:function(a,b){return h(a)(this,b)},
134 $evalAsync:function(a,b){p.$$phase||t.length||l.defer(function(){t.length&&p.$digest()});t.push({scope:this,expression:a,locals:b})},$$postDigest:function(a){w.push(a)},$apply:function(a){try{return r("$apply"),this.$eval(a)}catch(b){g(b)}finally{p.$$phase=null;try{p.$digest()}catch(c){throw g(c),c;}}},$applyAsync:function(a){function b(){c.$eval(a)}var c=this;a&&I.push(b);u()},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||
135 (d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);var e=this;return function(){var d=c.indexOf(b);-1!==d&&(c[d]=null,x(e,1,a))}},$emit:function(a,b){var c=[],d,e=this,f=!1,h={name:a,targetScope:e,stopPropagation:function(){f=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=cb([h],arguments,1),l,n;do{d=e.$$listeners[a]||c;h.currentScope=e;l=0;for(n=d.length;l<n;l++)if(d[l])try{d[l].apply(null,k)}catch(r){g(r)}else d.splice(l,1),l--,n--;if(f)return h.currentScope=
136 null,h;e=e.$parent}while(e);h.currentScope=null;return h},$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 f=cb([e],arguments,1),h,k;c=d;){e.currentScope=c;d=c.$$listeners[a]||[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,f)}catch(l){g(l)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=
137 c.$$nextSibling);)c=c.$parent}e.currentScope=null;return e}};var p=new n,t=p.$$asyncQueue=[],w=p.$$postDigestQueue=[],I=p.$$applyAsyncQueue=[];return p}]}function he(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*((https?|ftp|file|blob):|data:image\/)/;this.aHrefSanitizationWhitelist=function(a){return w(a)?(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return w(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,f;f=Ba(c).href;return""===f||f.match(e)?c:"unsafe:"+
138 f}}}function Zf(b){if("self"===b)return b;if(L(b)){if(-1<b.indexOf("***"))throw Da("iwcard",b);b=ud(b).replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return new RegExp("^"+b+"$")}if(Za(b))return new RegExp("^"+b.source+"$");throw Da("imatcher");}function vd(b){var a=[];w(b)&&m(b,function(b){a.push(Zf(b))});return a}function nf(){this.SCE_CONTEXTS=pa;var b=["self"],a=[];this.resourceUrlWhitelist=function(a){arguments.length&&(b=vd(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&
139 (a=vd(b));return a};this.$get=["$injector",function(c){function d(a,b){return"self"===a?gd(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 Da("unsafe");};c.has("$sanitize")&&(f=c.get("$sanitize"));var g=e(),h={};h[pa.HTML]=e(g);h[pa.CSS]=e(g);h[pa.URL]=
140 e(g);h[pa.JS]=e(g);h[pa.RESOURCE_URL]=e(h[pa.URL]);return{trustAs:function(a,b){var c=h.hasOwnProperty(a)?h[a]:null;if(!c)throw Da("icontext",a,b);if(null===b||b===t||""===b)return b;if("string"!==typeof b)throw Da("itype",a);return new c(b)},getTrusted:function(c,e){if(null===e||e===t||""===e)return e;var g=h.hasOwnProperty(c)?h[c]:null;if(g&&e instanceof g)return e.$$unwrapTrustedValue();if(c===pa.RESOURCE_URL){var g=Ba(e.toString()),r,s,m=!1;r=0;for(s=b.length;r<s;r++)if(d(b[r],g)){m=!0;break}if(m)for(r=
141 0,s=a.length;r<s;r++)if(d(a[r],g)){m=!1;break}if(m)return e;throw Da("insecurl",e.toString());}if(c===pa.HTML)return f(e);throw Da("unsafe");},valueOf:function(a){return a instanceof g?a.$$unwrapTrustedValue():a}}}]}function mf(){var b=!0;this.enabled=function(a){arguments.length&&(b=!!a);return b};this.$get=["$parse","$sceDelegate",function(a,c){if(b&&8>Ua)throw Da("iequirks");var d=ia(pa);d.isEnabled=function(){return b};d.trustAs=c.trustAs;d.getTrusted=c.getTrusted;d.valueOf=c.valueOf;b||(d.trustAs=
142 d.getTrusted=function(a,b){return b},d.valueOf=Ya);d.parseAs=function(b,c){var e=a(c);return e.literal&&e.constant?e:a(c,function(a){return d.getTrusted(b,a)})};var e=d.parseAs,f=d.getTrusted,g=d.trustAs;m(pa,function(a,b){var c=M(b);d[hb("parse_as_"+c)]=function(b){return e(a,b)};d[hb("get_trusted_"+c)]=function(b){return f(a,b)};d[hb("trust_as_"+c)]=function(b){return g(a,b)}});return d}]}function of(){this.$get=["$window","$document",function(b,a){var c={},d=W((/android (\d+)/.exec(M((b.navigator||
143 {}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),f=a[0]||{},g,h=/^(Moz|webkit|ms)(?=[A-Z])/,l=f.body&&f.body.style,k=!1,n=!1;if(l){for(var r in l)if(k=h.exec(r)){g=k[0];g=g.substr(0,1).toUpperCase()+g.substr(1);break}g||(g="WebkitOpacity"in l&&"webkit");k=!!("transition"in l||g+"Transition"in l);n=!!("animation"in l||g+"Animation"in l);!d||k&&n||(k=L(l.webkitTransition),n=L(l.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hasEvent:function(a){if("input"===
144 a&&11>=Ua)return!1;if(A(c[a])){var b=f.createElement("div");c[a]="on"+a in b}return c[a]},csp:fb(),vendorPrefix:g,transitions:k,animations:n,android:d}}]}function qf(){this.$get=["$templateCache","$http","$q","$sce",function(b,a,c,d){function e(f,g){e.totalPendingRequests++;L(f)&&b.get(f)||(f=d.getTrustedResourceUrl(f));var h=a.defaults&&a.defaults.transformResponse;G(h)?h=h.filter(function(a){return a!==$b}):h===$b&&(h=null);return a.get(f,{cache:b,transformResponse:h})["finally"](function(){e.totalPendingRequests--}).then(function(a){b.put(f,
145 a.data);return a.data},function(a){if(!g)throw ea("tpload",f,a.status,a.statusText);return c.reject(a)})}e.totalPendingRequests=0;return e}]}function rf(){this.$get=["$rootScope","$browser","$location",function(b,a,c){return{findBindings:function(a,b,c){a=a.getElementsByClassName("ng-binding");var g=[];m(a,function(a){var d=ca.element(a).data("$binding");d&&m(d,function(d){c?(new RegExp("(^|\\s)"+ud(b)+"(\\s|\\||$)")).test(d)&&g.push(a):-1!=d.indexOf(b)&&g.push(a)})});return g},findModels:function(a,
146 b,c){for(var g=["ng-","data-ng-","ng\\:"],h=0;h<g.length;++h){var l=a.querySelectorAll("["+g[h]+"model"+(c?"=":"*=")+'"'+b+'"]');if(l.length)return l}},getLocation:function(){return c.url()},setLocation:function(a){a!==c.url()&&(c.url(a),b.$digest())},whenStable:function(b){a.notifyWhenNoOutstandingRequests(b)}}}]}function sf(){this.$get=["$rootScope","$browser","$q","$$q","$exceptionHandler",function(b,a,c,d,e){function f(f,l,k){z(f)||(k=l,l=f,f=v);var n=za.call(arguments,3),r=w(k)&&!k,s=(r?d:c).defer(),
147 m=s.promise,q;q=a.defer(function(){try{s.resolve(f.apply(null,n))}catch(a){s.reject(a),e(a)}finally{delete g[m.$$timeoutId]}r||b.$apply()},l);m.$$timeoutId=q;g[q]=s;return m}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 Ba(b){Ua&&(X.setAttribute("href",b),b=X.href);X.setAttribute("href",b);return{href:X.href,protocol:X.protocol?X.protocol.replace(/:$/,""):"",host:X.host,
148 search:X.search?X.search.replace(/^\?/,""):"",hash:X.hash?X.hash.replace(/^#/,""):"",hostname:X.hostname,port:X.port,pathname:"/"===X.pathname.charAt(0)?X.pathname:"/"+X.pathname}}function gd(b){b=L(b)?Ba(b):b;return b.protocol===wd.protocol&&b.host===wd.host}function tf(){this.$get=ra(O)}function xd(b){function a(a){try{return decodeURIComponent(a)}catch(b){return a}}var c=b[0]||{},d={},e="";return function(){var b,g,h,l,k;b=c.cookie||"";if(b!==e)for(e=b,b=e.split("; "),d={},h=0;h<b.length;h++)g=
149 b[h],l=g.indexOf("="),0<l&&(k=a(g.substring(0,l)),d[k]===t&&(d[k]=a(g.substring(l+1))));return d}}function xf(){this.$get=xd}function Lc(b){function a(c,d){if(H(c)){var e={};m(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",yd);a("date",zd);a("filter",$f);a("json",ag);a("limitTo",bg);a("lowercase",cg);a("number",Ad);a("orderBy",Bd);a("uppercase",dg)}function $f(){return function(b,
150 a,c){if(!Ea(b)){if(null==b)return b;throw J("filter")("notarray",b);}var d;switch(ic(a)){case "function":break;case "boolean":case "null":case "number":case "string":d=!0;case "object":a=eg(a,c,d);break;default:return b}return Array.prototype.filter.call(b,a)}}function eg(b,a,c){var d=H(b)&&"$"in b;!0===a?a=ka:z(a)||(a=function(a,b){if(A(a))return!1;if(null===a||null===b)return a===b;if(H(b)||H(a)&&!rc(a))return!1;a=M(""+a);b=M(""+b);return-1!==a.indexOf(b)});return function(e){return d&&!H(e)?La(e,
151 b.$,a,!1):La(e,b,a,c)}}function La(b,a,c,d,e){var f=ic(b),g=ic(a);if("string"===g&&"!"===a.charAt(0))return!La(b,a.substring(1),c,d);if(G(b))return b.some(function(b){return La(b,a,c,d)});switch(f){case "object":var h;if(d){for(h in b)if("$"!==h.charAt(0)&&La(b[h],a,c,!0))return!0;return e?!1:La(b,a,c,!1)}if("object"===g){for(h in a)if(e=a[h],!z(e)&&!A(e)&&(f="$"===h,!La(f?b:b[h],e,c,f,f)))return!1;return!0}return c(b,a);case "function":return!1;default:return c(b,a)}}function ic(b){return null===
152 b?"null":typeof b}function yd(b){var a=b.NUMBER_FORMATS;return function(b,d,e){A(d)&&(d=a.CURRENCY_SYM);A(e)&&(e=a.PATTERNS[1].maxFrac);return null==b?b:Cd(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,e).replace(/\u00A4/g,d)}}function Ad(b){var a=b.NUMBER_FORMATS;return function(b,d){return null==b?b:Cd(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}function Cd(b,a,c,d,e){if(H(b))return"";var f=0>b;b=Math.abs(b);var g=Infinity===b;if(!g&&!isFinite(b))return"";var h=b+"",l="",k=!1,n=[];g&&(l="\u221e");
153 if(!g&&-1!==h.indexOf("e")){var r=h.match(/([\d\.]+)e(-?)(\d+)/);r&&"-"==r[2]&&r[3]>e+1?b=0:(l=h,k=!0)}if(g||k)0<e&&1>b&&(l=b.toFixed(e),b=parseFloat(l));else{g=(h.split(Dd)[1]||"").length;A(e)&&(e=Math.min(Math.max(a.minFrac,g),a.maxFrac));b=+(Math.round(+(b.toString()+"e"+e)).toString()+"e"+-e);var g=(""+b).split(Dd),h=g[0],g=g[1]||"",r=0,s=a.lgSize,m=a.gSize;if(h.length>=s+m)for(r=h.length-s,k=0;k<r;k++)0===(r-k)%m&&0!==k&&(l+=c),l+=h.charAt(k);for(k=r;k<h.length;k++)0===(h.length-k)%s&&0!==k&&
154 (l+=c),l+=h.charAt(k);for(;g.length<e;)g+="0";e&&"0"!==e&&(l+=d+g.substr(0,e))}0===b&&(f=!1);n.push(f?a.negPre:a.posPre,l,f?a.negSuf:a.posSuf);return n.join("")}function Gb(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 Y(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 Gb(e,a,d)}}function Hb(b,a){return function(c,d){var e=c["get"+b](),f=rb(a?"SHORT"+b:b);return d[f][e]}}function Ed(b){var a=
155 (new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function Fd(b){return function(a){var c=Ed(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return Gb(a,b)}}function jc(b,a){return 0>=b.getFullYear()?a.ERAS[0]:a.ERAS[1]}function zd(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,l=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=W(b[9]+b[10]),g=W(b[9]+b[11]));h.call(a,W(b[1]),
156 W(b[2])-1,W(b[3]));f=W(b[4]||0)-f;g=W(b[5]||0)-g;h=W(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));l.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=[],l,k;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;L(c)&&(c=fg.test(c)?W(c):a(c));V(c)&&(c=new Date(c));if(!aa(c)||!isFinite(c.getTime()))return c;for(;e;)(k=gg.exec(e))?(h=cb(h,k,1),e=h.pop()):(h.push(e),e=null);var n=c.getTimezoneOffset();
157 f&&(n=xc(f,c.getTimezoneOffset()),c=Pb(c,f,!0));m(h,function(a){l=hg[a];g+=l?l(c,b.DATETIME_FORMATS,n):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ag(){return function(b,a){A(a)&&(a=2);return db(b,a)}}function bg(){return function(b,a,c){a=Infinity===Math.abs(Number(a))?Number(a):W(a);if(isNaN(a))return b;V(b)&&(b=b.toString());if(!G(b)&&!L(b))return b;c=!c||isNaN(c)?0:W(c);c=0>c&&c>=-b.length?b.length+c:c;return 0<=a?b.slice(c,c+a):0===c?b.slice(a,b.length):b.slice(Math.max(0,
158 c+a),c)}}function Bd(b){function a(a,c){c=c?-1:1;return a.map(function(a){var d=1,h=Ya;if(z(a))h=a;else if(L(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))d="-"==a.charAt(0)?-1:1,a=a.substring(1);if(""!==a&&(h=b(a),h.constant))var l=h(),h=function(a){return a[l]}}return{get:h,descending:d*c}})}function c(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}return function(b,e,f){if(!Ea(b))return b;G(e)||(e=[e]);0===e.length&&(e=["+"]);var g=a(e,f);b=Array.prototype.map.call(b,
159 function(a,b){return{value:a,predicateValues:g.map(function(d){var e=d.get(a);d=typeof e;if(null===e)d="string",e="null";else if("string"===d)e=e.toLowerCase();else if("object"===d)a:{if("function"===typeof e.valueOf&&(e=e.valueOf(),c(e)))break a;if(rc(e)&&(e=e.toString(),c(e)))break a;e=b}return{value:e,type:d}})}});b.sort(function(a,b){for(var c=0,d=0,e=g.length;d<e;++d){var c=a.predicateValues[d],f=b.predicateValues[d],m=0;c.type===f.type?c.value!==f.value&&(m=c.value<f.value?-1:1):m=c.type<f.type?
160 -1:1;if(c=m*g[d].descending)break}return c});return b=b.map(function(a){return a.value})}}function Ma(b){z(b)&&(b={link:b});b.restrict=b.restrict||"AC";return ra(b)}function Gd(b,a,c,d,e){var f=this,g=[],h=f.$$parentForm=b.parent().controller("form")||Ib;f.$error={};f.$$success={};f.$pending=t;f.$name=e(a.name||a.ngForm||"")(c);f.$dirty=!1;f.$pristine=!0;f.$valid=!0;f.$invalid=!1;f.$submitted=!1;h.$addControl(f);f.$rollbackViewValue=function(){m(g,function(a){a.$rollbackViewValue()})};f.$commitViewValue=
161 function(){m(g,function(a){a.$commitViewValue()})};f.$addControl=function(a){Ra(a.$name,"input");g.push(a);a.$name&&(f[a.$name]=a)};f.$$renameControl=function(a,b){var c=a.$name;f[c]===a&&delete f[c];f[b]=a;a.$name=b};f.$removeControl=function(a){a.$name&&f[a.$name]===a&&delete f[a.$name];m(f.$pending,function(b,c){f.$setValidity(c,null,a)});m(f.$error,function(b,c){f.$setValidity(c,null,a)});m(f.$$success,function(b,c){f.$setValidity(c,null,a)});bb(g,a)};Hd({ctrl:this,$element:b,set:function(a,b,
162 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&&(bb(d,c),0===d.length&&delete a[b])},parentForm:h,$animate:d});f.$setDirty=function(){d.removeClass(b,Va);d.addClass(b,Jb);f.$dirty=!0;f.$pristine=!1;h.$setDirty()};f.$setPristine=function(){d.setClass(b,Va,Jb+" ng-submitted");f.$dirty=!1;f.$pristine=!0;f.$submitted=!1;m(g,function(a){a.$setPristine()})};f.$setUntouched=function(){m(g,function(a){a.$setUntouched()})};f.$setSubmitted=function(){d.addClass(b,
163 "ng-submitted");f.$submitted=!0;h.$setSubmitted()}}function kc(b){b.$formatters.push(function(a){return b.$isEmpty(a)?a:a.toString()})}function kb(b,a,c,d,e,f){var g=M(a[0].type);if(!e.android){var h=!1;a.on("compositionstart",function(a){h=!0});a.on("compositionend",function(){h=!1;l()})}var l=function(b){k&&(f.defer.cancel(k),k=null);if(!h){var e=a.val();b=b&&b.type;"password"===g||c.ngTrim&&"false"===c.ngTrim||(e=R(e));(d.$viewValue!==e||""===e&&d.$$hasNativeValidators)&&d.$setViewValue(e,b)}};
164 if(e.hasEvent("input"))a.on("input",l);else{var k,n=function(a,b,c){k||(k=f.defer(function(){k=null;b&&b.value===c||l(a)}))};a.on("keydown",function(a){var b=a.keyCode;91===b||15<b&&19>b||37<=b&&40>=b||n(a,this,this.value)});if(e.hasEvent("paste"))a.on("paste cut",n)}a.on("change",l);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)}}function Kb(b,a){return function(c,d){var e,f;if(aa(c))return c;if(L(c)){'"'==c.charAt(0)&&'"'==c.charAt(c.length-1)&&(c=c.substring(1,c.length-1));
165 if(ig.test(c))return new Date(c);b.lastIndex=0;if(e=b.exec(c))return e.shift(),f=d?{yyyy:d.getFullYear(),MM:d.getMonth()+1,dd:d.getDate(),HH:d.getHours(),mm:d.getMinutes(),ss:d.getSeconds(),sss:d.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},m(e,function(b,c){c<a.length&&(f[a[c]]=+b)}),new Date(f.yyyy,f.MM-1,f.dd,f.HH,f.mm,f.ss||0,1E3*f.sss||0)}return NaN}}function lb(b,a,c,d){return function(e,f,g,h,l,k,n){function r(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function s(a){return w(a)?
166 aa(a)?a:c(a):t}Id(e,f,g,h);kb(e,f,g,h,l,k);var m=h&&h.$options&&h.$options.timezone,q;h.$$parserName=b;h.$parsers.push(function(b){return h.$isEmpty(b)?null:a.test(b)?(b=c(b,q),m&&(b=Pb(b,m)),b):t});h.$formatters.push(function(a){if(a&&!aa(a))throw Lb("datefmt",a);if(r(a))return(q=a)&&m&&(q=Pb(q,m,!0)),n("date")(a,d,m);q=null;return""});if(w(g.min)||g.ngMin){var F;h.$validators.min=function(a){return!r(a)||A(F)||c(a)>=F};g.$observe("min",function(a){F=s(a);h.$validate()})}if(w(g.max)||g.ngMax){var u;
167 h.$validators.max=function(a){return!r(a)||A(u)||c(a)<=u};g.$observe("max",function(a){u=s(a);h.$validate()})}}}function Id(b,a,c,d){(d.$$hasNativeValidators=H(a[0].validity))&&d.$parsers.push(function(b){var c=a.prop("validity")||{};return c.badInput&&!c.typeMismatch?t:b})}function Jd(b,a,c,d,e){if(w(d)){b=b(d);if(!b.constant)throw J("ngModel")("constexpr",c,d);return b(a)}return e}function lc(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=
168 a[d],n=0;n<b.length;n++)if(e==b[n])continue a;c.push(e)}return c}function e(a){var b=[];return G(a)?(m(a,function(a){b=b.concat(e(a))}),b):L(a)?a.split(" "):H(a)?(m(a,function(a,c){a&&(b=b.concat(c.split(" ")))}),b):a}return{restrict:"AC",link:function(f,g,h){function l(a,b){var c=g.data("$classCounts")||ga(),d=[];m(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 m=
169 l(k,1);h.$addClass(m)}else if(!ka(b,n)){var q=e(n),m=d(k,q),k=d(q,k),m=l(m,1),k=l(k,-1);m&&m.length&&c.addClass(g,m);k&&k.length&&c.removeClass(g,k)}}n=ia(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=l(k,1),h.$addClass(g)):(g=l(k,-1),h.$removeClass(g))}})}}}]}function Hd(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)}
170 function c(b,c){b=b?"-"+Bc(b,"-"):"";a(mb+b,!0===c);a(Kd+b,!1===c)}var d=b.ctrl,e=b.$element,f={},g=b.set,h=b.unset,l=b.parentForm,k=b.$animate;f[Kd]=!(f[mb]=e.hasClass(mb));d.$setValidity=function(b,e,f){e===t?(d.$pending||(d.$pending={}),g(d.$pending,b,f)):(d.$pending&&h(d.$pending,b,f),Ld(d.$pending)&&(d.$pending=t));ab(e)?e?(h(d.$error,b,f),g(d.$$success,b,f)):(g(d.$error,b,f),h(d.$$success,b,f)):(h(d.$error,b,f),h(d.$$success,b,f));d.$pending?(a(Md,!0),d.$valid=d.$invalid=t,c("",null)):(a(Md,
171 !1),d.$valid=Ld(d.$error),d.$invalid=!d.$valid,c("",d.$valid));e=d.$pending&&d.$pending[b]?t:d.$error[b]?!1:d.$$success[b]?!0:null;c(b,e);l.$setValidity(b,e,d)}}function Ld(b){if(b)for(var a in b)if(b.hasOwnProperty(a))return!1;return!0}var jg=/^\/(.+)\/([a-z]*)$/,M=function(b){return L(b)?b.toLowerCase():b},Xa=Object.prototype.hasOwnProperty,rb=function(b){return L(b)?b.toUpperCase():b},Ua,y,la,za=[].slice,Mf=[].splice,kg=[].push,sa=Object.prototype.toString,sc=Object.getPrototypeOf,Fa=J("ng"),ca=
172 O.angular||(O.angular={}),gb,nb=0;Ua=U.documentMode;v.$inject=[];Ya.$inject=[];var G=Array.isArray,uc=/^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/,R=function(b){return L(b)?b.trim():b},ud=function(b){return b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08")},fb=function(){if(w(fb.isActive_))return fb.isActive_;var b=!(!U.querySelector("[ng-csp]")&&!U.querySelector("[data-ng-csp]"));if(!b)try{new Function("")}catch(a){b=!0}return fb.isActive_=
173 b},pb=function(){if(w(pb.name_))return pb.name_;var b,a,c=Oa.length,d,e;for(a=0;a<c;++a)if(d=Oa[a],b=U.querySelector("["+d.replace(":","\\:")+"jq]")){e=b.getAttribute(d+"jq");break}return pb.name_=e},Oa=["ng-","data-ng-","ng:","x-ng-"],be=/[A-Z]/g,Cc=!1,Rb,qa=1,Na=3,fe={full:"1.4.3",major:1,minor:4,dot:3,codeName:"foam-acceleration"};Q.expando="ng339";var ib=Q.cache={},Df=1;Q._data=function(b){return this.cache[b[this.expando]]||{}};var yf=/([\:\-\_]+(.))/g,zf=/^moz([A-Z])/,lg={mouseleave:"mouseout",
174 mouseenter:"mouseover"},Ub=J("jqLite"),Cf=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Tb=/<|&#?\w+;/,Af=/<([\w:]+)/,Bf=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,na={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,"",""]};na.optgroup=na.option;na.tbody=na.tfoot=na.colgroup=na.caption=na.thead;
175 na.th=na.td;var Pa=Q.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===U.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),Q(O).on("load",a))},toString:function(){var b=[];m(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:kg,sort:[].sort,splice:[].splice},Ab={};m("multiple selected checked disabled readOnly required open".split(" "),function(b){Ab[M(b)]=b});var Tc={};m("input select option textarea button form details".split(" "),
176 function(b){Tc[b]=!0});var Uc={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern"};m({data:Wb,removeData:ub,hasData:function(b){for(var a in ib[b.ng339])return!0;return!1}},function(b,a){Q[a]=b});m({data:Wb,inheritedData:zb,scope:function(b){return y.data(b,"$scope")||zb(b.parentNode||b,["$isolateScope","$scope"])},isolateScope:function(b){return y.data(b,"$isolateScope")||y.data(b,"$isolateScopeNoTemplate")},controller:Qc,injector:function(b){return zb(b,
177 "$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:wb,css:function(b,a,c){a=hb(a);if(w(c))b.style[a]=c;else return b.style[a]},attr:function(b,a,c){var d=b.nodeType;if(d!==Na&&2!==d&&8!==d)if(d=M(a),Ab[d])if(w(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d));else return b[a]||(b.attributes.getNamedItem(a)||v).specified?d:t;else if(w(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?t:b},prop:function(b,a,c){if(w(c))b[a]=c;else return b[a]},
178 text:function(){function b(a,b){if(A(b)){var d=a.nodeType;return d===qa||d===Na?a.textContent:""}a.textContent=b}b.$dv="";return b}(),val:function(b,a){if(A(a)){if(b.multiple&&"select"===ta(b)){var c=[];m(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(A(a))return b.innerHTML;tb(b,!0);b.innerHTML=a},empty:Rc},function(b,a){Q.prototype[a]=function(a,d){var e,f,g=this.length;if(b!==Rc&&(2==b.length&&b!==wb&&b!==Qc?
179 a:d)===t){if(H(a)){for(e=0;e<g;e++)if(b===Wb)b(this[e],a);else for(f in a)b(this[e],f,a[f]);return this}e=b.$dv;g=e===t?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}});m({removeData:ub,on:function a(c,d,e,f){if(w(f))throw Ub("onargs");if(Mc(c)){var g=vb(c,!0);f=g.events;var h=g.handle;h||(h=g.handle=Gf(c,f));for(var g=0<=d.indexOf(" ")?d.split(" "):[d],l=g.length;l--;){d=g[l];var k=f[d];k||(f[d]=[],"mouseenter"===d||"mouseleave"===
180 d?a(c,lg[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:Pc,one:function(a,c,d){a=y(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;tb(a);m(new Q(c),function(c){d?e.insertBefore(c,d.nextSibling):e.replaceChild(c,a);d=c})},children:function(a){var c=[];m(a.childNodes,function(a){a.nodeType===qa&&c.push(a)});return c},contents:function(a){return a.contentDocument||
181 a.childNodes||[]},append:function(a,c){var d=a.nodeType;if(d===qa||11===d){c=new Q(c);for(var d=0,e=c.length;d<e;d++)a.appendChild(c[d])}},prepend:function(a,c){if(a.nodeType===qa){var d=a.firstChild;m(new Q(c),function(c){a.insertBefore(c,d)})}},wrap:function(a,c){c=y(c).eq(0).clone()[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:Xb,detach:function(a){Xb(a,!0)},after:function(a,c){var d=a,e=a.parentNode;c=new Q(c);for(var f=0,g=c.length;f<g;f++){var h=c[f];e.insertBefore(h,
182 d.nextSibling);d=h}},addClass:yb,removeClass:xb,toggleClass:function(a,c,d){c&&m(c.split(" "),function(c){var f=d;A(f)&&(f=!wb(a,c));(f?yb:xb)(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:Vb,triggerHandler:function(a,c,d){var e,f,g=c.type||c,h=vb(a);if(h=(h=h&&h.events)&&h[g])e={preventDefault:function(){this.defaultPrevented=!0},isDefaultPrevented:function(){return!0===
183 this.defaultPrevented},stopImmediatePropagation:function(){this.immediatePropagationStopped=!0},isImmediatePropagationStopped:function(){return!0===this.immediatePropagationStopped},stopPropagation:v,type:g,target:a},c.type&&(e=P(e,c)),c=ia(h),f=d?[e].concat(d):[e],m(c,function(c){e.isImmediatePropagationStopped()||c.apply(a,f)})}},function(a,c){Q.prototype[c]=function(c,e,f){for(var g,h=0,l=this.length;h<l;h++)A(g)?(g=a(this[h],c,e,f),w(g)&&(g=y(g))):Oc(g,a(this[h],c,e,f));return w(g)?g:this};Q.prototype.bind=
184 Q.prototype.on;Q.prototype.unbind=Q.prototype.off});Sa.prototype={put:function(a,c){this[Ga(a,this.nextUid)]=c},get:function(a){return this[Ga(a,this.nextUid)]},remove:function(a){var c=this[a=Ga(a,this.nextUid)];delete this[a];return c}};var wf=[function(){this.$get=[function(){return Sa}]}],Wc=/^function\s*[^\(]*\(\s*([^\)]*)\)/m,mg=/,/,ng=/^\s*(_?)(\S+?)\1\s*$/,Vc=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ha=J("$injector");eb.$$annotate=function(a,c,d){var e;if("function"===typeof a){if(!(e=a.$inject)){e=
185 [];if(a.length){if(c)throw L(d)&&d||(d=a.name||Hf(a)),Ha("strictdi",d);c=a.toString().replace(Vc,"");c=c.match(Wc);m(c[1].split(mg),function(a){a.replace(ng,function(a,c,d){e.push(d)})})}a.$inject=e}}else G(a)?(c=a.length-1,Qa(a[c],"fn"),e=a.slice(0,c)):Qa(a,"fn",!0);return e};var Nd=J("$animate"),Ue=function(){this.$get=["$q","$$rAF",function(a,c){function d(){}d.all=v;d.chain=v;d.prototype={end:v,cancel:v,resume:v,pause:v,complete:v,then:function(d,f){return a(function(a){c(function(){a()})}).then(d,
186 f)}};return d}]},Te=function(){var a=new Sa,c=[];this.$get=["$$AnimateRunner","$rootScope",function(d,e){function f(d,f,l){var k=a.get(d);k||(a.put(d,k={}),c.push(d));f&&m(f.split(" "),function(a){a&&(k[a]=!0)});l&&m(l.split(" "),function(a){a&&(k[a]=!1)});1<c.length||e.$$postDigest(function(){m(c,function(c){var d=a.get(c);if(d){var e=If(c.attr("class")),f="",g="";m(d,function(a,c){a!==!!e[c]&&(a?f+=(f.length?" ":"")+c:g+=(g.length?" ":"")+c)});m(c,function(a){f&&yb(a,f);g&&xb(a,g)});a.remove(c)}});
187 c.length=0})}return{enabled:v,on:v,off:v,pin:v,push:function(a,c,e,k){k&&k();e=e||{};e.from&&a.css(e.from);e.to&&a.css(e.to);(e.addClass||e.removeClass)&&f(a,e.addClass,e.removeClass);return new d}}}]},Se=["$provide",function(a){var c=this;this.$$registeredAnimations=Object.create(null);this.register=function(d,e){if(d&&"."!==d.charAt(0))throw Nd("notcsel",d);var f=d+"-animation";c.$$registeredAnimations[d.substr(1)]=f;a.factory(f,e)};this.classNameFilter=function(a){if(1===arguments.length&&(this.$$classNameFilter=
188 a instanceof RegExp?a:null)&&/(\s+|\/)ng-animate(\s+|\/)/.test(this.$$classNameFilter.toString()))throw Nd("nongcls","ng-animate");return this.$$classNameFilter};this.$get=["$$animateQueue",function(a){function c(a,d,e){if(e){var l;a:{for(l=0;l<e.length;l++){var k=e[l];if(1===k.nodeType){l=k;break a}}l=void 0}!l||l.parentNode||l.previousElementSibling||(e=null)}e?e.after(a):d.prepend(a)}return{on:a.on,off:a.off,pin:a.pin,enabled:a.enabled,cancel:function(a){a.end&&a.end()},enter:function(f,g,h,l){g=
189 g&&y(g);h=h&&y(h);g=g||h.parent();c(f,g,h);return a.push(f,"enter",Ia(l))},move:function(f,g,h,l){g=g&&y(g);h=h&&y(h);g=g||h.parent();c(f,g,h);return a.push(f,"move",Ia(l))},leave:function(c,e){return a.push(c,"leave",Ia(e),function(){c.remove()})},addClass:function(c,e,h){h=Ia(h);h.addClass=jb(h.addclass,e);return a.push(c,"addClass",h)},removeClass:function(c,e,h){h=Ia(h);h.removeClass=jb(h.removeClass,e);return a.push(c,"removeClass",h)},setClass:function(c,e,h,l){l=Ia(l);l.addClass=jb(l.addClass,
190 e);l.removeClass=jb(l.removeClass,h);return a.push(c,"setClass",l)},animate:function(c,e,h,l,k){k=Ia(k);k.from=k.from?P(k.from,e):e;k.to=k.to?P(k.to,h):h;k.tempClasses=jb(k.tempClasses,l||"ng-inline-animate");return a.push(c,"animate",k)}}}]}],ea=J("$compile");Ec.$inject=["$provide","$$sanitizeUriProvider"];var Zc=/^((?:x|data)[\:\-_])/i,Nf=J("$controller"),Xc=/^(\S+)(\s+as\s+(\w+))?$/,cd="application/json",ac={"Content-Type":cd+";charset=utf-8"},Pf=/^\[|^\{(?!\{)/,Qf={"[":/]$/,"{":/}$/},Of=/^\)\]\}',?\n/,
191 Ka=ca.$interpolateMinErr=J("$interpolate");Ka.throwNoconcat=function(a){throw Ka("noconcat",a);};Ka.interr=function(a,c){return Ka("interr",a,c.toString())};var og=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,Tf={http:80,https:443,ftp:21},Cb=J("$location"),pg={$$html5:!1,$$replace:!1,absUrl:Db("$$absUrl"),url:function(a){if(A(a))return this.$$url;var c=og.exec(a);(c[1]||""===a)&&this.path(decodeURIComponent(c[1]));(c[2]||c[1]||""===a)&&this.search(c[3]||"");this.hash(c[5]||"");return this},protocol:Db("$$protocol"),
192 host:Db("$$host"),port:Db("$$port"),path:kd("$$path",function(a){a=null!==a?a.toString():"";return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;case 1:if(L(a)||V(a))a=a.toString(),this.$$search=zc(a);else if(H(a))a=fa(a,{}),m(a,function(c,e){null==c&&delete a[e]}),this.$$search=a;else throw Cb("isrcharg");break;default:A(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:kd("$$hash",function(a){return null!==
193 a?a.toString():""}),replace:function(){this.$$replace=!0;return this}};m([jd,ec,dc],function(a){a.prototype=Object.create(pg);a.prototype.state=function(c){if(!arguments.length)return this.$$state;if(a!==dc||!this.$$html5)throw Cb("nostate");this.$$state=A(c)?null:c;return this}});var da=J("$parse"),Uf=Function.prototype.call,Vf=Function.prototype.apply,Wf=Function.prototype.bind,Mb=ga();m("+ - * / % === !== == != < > <= >= && || ! = |".split(" "),function(a){Mb[a]=!0});var qg={n:"\n",f:"\f",r:"\r",
194 t:"\t",v:"\v","'":"'",'"':'"'},gc=function(a){this.options=a};gc.prototype={constructor:gc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index<this.text.length;)if(a=this.text.charAt(this.index),'"'===a||"'"===a)this.readString(a);else if(this.isNumber(a)||"."===a&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(a))this.readIdent();else if(this.is(a,"(){}[].,;:?"))this.tokens.push({index:this.index,text:a}),this.index++;else if(this.isWhitespace(a))this.index++;
195 else{var c=a+this.peek(),d=c+this.peek(2),e=Mb[c],f=Mb[d];Mb[a]||e||f?(a=f?d:e?c:a,this.tokens.push({index:this.index,text:a,operator:!0}),this.index+=a.length):this.throwError("Unexpected next character ",this.index,this.index+1)}return this.tokens},is:function(a,c){return-1!==c.indexOf(a)},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&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||
196 "\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=w(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw da("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index<this.text.length;){var d=M(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();
197 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++}this.tokens.push({index:c,text:a,constant:!0,value:Number(a)})},readIdent:function(){for(var a=this.index;this.index<this.text.length;){var c=this.text.charAt(this.index);if(!this.isIdent(c)&&!this.isNumber(c))break;this.index++}this.tokens.push({index:a,
198 text:this.text.slice(a,this.index),identifier:!0})},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+=qg[g]||g,f=!1;else if("\\"===g)f=!0;else{if(g===a){this.index++;this.tokens.push({index:c,text:e,constant:!0,
199 value:d});return}d+=g}this.index++}this.throwError("Unterminated quote",c)}};var q=function(a,c){this.lexer=a;this.options=c};q.Program="Program";q.ExpressionStatement="ExpressionStatement";q.AssignmentExpression="AssignmentExpression";q.ConditionalExpression="ConditionalExpression";q.LogicalExpression="LogicalExpression";q.BinaryExpression="BinaryExpression";q.UnaryExpression="UnaryExpression";q.CallExpression="CallExpression";q.MemberExpression="MemberExpression";q.Identifier="Identifier";q.Literal=
200 "Literal";q.ArrayExpression="ArrayExpression";q.Property="Property";q.ObjectExpression="ObjectExpression";q.ThisExpression="ThisExpression";q.NGValueParameter="NGValueParameter";q.prototype={ast:function(a){this.text=a;this.tokens=this.lexer.lex(a);a=this.program();0!==this.tokens.length&&this.throwError("is an unexpected token",this.tokens[0]);return a},program:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.expressionStatement()),!this.expect(";"))return{type:q.Program,
201 body:a}},expressionStatement:function(){return{type:q.ExpressionStatement,expression:this.filterChain()}},filterChain:function(){for(var a=this.expression();this.expect("|");)a=this.filter(a);return a},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary();this.expect("=")&&(a={type:q.AssignmentExpression,left:a,right:this.assignment(),operator:"="});return a},ternary:function(){var a=this.logicalOR(),c,d;return this.expect("?")&&(c=this.expression(),this.consume(":"))?
202 (d=this.expression(),{type:q.ConditionalExpression,test:a,alternate:c,consequent:d}):a},logicalOR:function(){for(var a=this.logicalAND();this.expect("||");)a={type:q.LogicalExpression,operator:"||",left:a,right:this.logicalAND()};return a},logicalAND:function(){for(var a=this.equality();this.expect("&&");)a={type:q.LogicalExpression,operator:"&&",left:a,right:this.equality()};return a},equality:function(){for(var a=this.relational(),c;c=this.expect("==","!=","===","!==");)a={type:q.BinaryExpression,
203 operator:c.text,left:a,right:this.relational()};return a},relational:function(){for(var a=this.additive(),c;c=this.expect("<",">","<=",">=");)a={type:q.BinaryExpression,operator:c.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a={type:q.BinaryExpression,operator:c.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a={type:q.BinaryExpression,operator:c.text,
204 left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:q.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.constants.hasOwnProperty(this.peek().text)?a=fa(this.constants[this.consume().text]):this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():
205 this.throwError("not a primary expression",this.peek());for(var c;c=this.expect("(","[",".");)"("===c.text?(a={type:q.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===c.text?(a={type:q.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===c.text?a={type:q.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var c={type:q.CallExpression,callee:this.identifier(),
206 arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return c},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.expression());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:q.Identifier,name:a.text}},constant:function(){return{type:q.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;
207 a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:q.ArrayExpression,elements:a}},object:function(){var a=[],c;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;c={type:q.Property,kind:"init"};this.peek().constant?c.key=this.constant():this.peek().identifier?c.key=this.identifier():this.throwError("invalid key",this.peek());this.consume(":");c.value=this.expression();a.push(c)}while(this.expect(","))}this.consume("}");return{type:q.ObjectExpression,properties:a}},
208 throwError:function(a,c){throw da("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},consume:function(a){if(0===this.tokens.length)throw da("ueoe",this.text);var c=this.expect(a);c||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return c},peekToken:function(){if(0===this.tokens.length)throw da("ueoe",this.text);return this.tokens[0]},peek:function(a,c,d,e){return this.peekAhead(0,a,c,d,e)},peekAhead:function(a,c,d,e,f){if(this.tokens.length>a){a=this.tokens[a];
209 var g=a.text;if(g===c||g===d||g===e||g===f||!(c||d||e||f))return a}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,e))?(this.tokens.shift(),a):!1},constants:{"true":{type:q.Literal,value:!0},"false":{type:q.Literal,value:!1},"null":{type:q.Literal,value:null},undefined:{type:q.Literal,value:t},"this":{type:q.ThisExpression}}};rd.prototype={compile:function(a,c){var d=this,e=this.astBuilder.ast(a);this.state={nextId:0,filters:{},expensiveChecks:c,fn:{vars:[],body:[],own:{}},assign:{vars:[],
210 body:[],own:{}},inputs:[]};T(e,d.$filter);var f="",g;this.stage="assign";if(g=pd(e))this.state.computing="assign",f=this.nextId(),this.recurse(g,f),f="fn.assign="+this.generateFunction("assign","s,v,l");g=nd(e.body);d.stage="inputs";m(g,function(a,c){var e="fn"+c;d.state[e]={vars:[],body:[],own:{}};d.state.computing=e;var f=d.nextId();d.recurse(a,f);d.return_(f);d.state.inputs.push(e);a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(e);f='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+
211 "var fn="+this.generateFunction("fn","s,l,a,i")+f+this.watchFns()+"return fn;";f=(new Function("$filter","ensureSafeMemberName","ensureSafeObject","ensureSafeFunction","ifDefined","plus","text",f))(this.$filter,Ca,oa,ld,Xf,md,a);this.state=this.stage=t;f.literal=qd(e);f.constant=e.constant;return f},USE:"use",STRICT:"strict",watchFns:function(){var a=[],c=this.state.inputs,d=this;m(c,function(c){a.push("var "+c+"="+d.generateFunction(c,"s"))});c.length&&a.push("fn.inputs=["+c.join(",")+"];");return a.join("")},
212 generateFunction:function(a,c){return"function("+c+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],c=this;m(this.state.filters,function(d,e){a.push(d+"=$filter("+c.escape(e)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")},recurse:function(a,c,d,e,f,g){var h,l,k=this,n,r;e=e||v;if(!g&&w(a.watchId))c=c||this.nextId(),this.if_("i",
213 this.lazyAssign(c,this.computedMember("i",a.watchId)),this.lazyRecurse(a,c,d,e,f,!0));else switch(a.type){case q.Program:m(a.body,function(c,d){k.recurse(c.expression,t,t,function(a){l=a});d!==a.body.length-1?k.current().body.push(l,";"):k.return_(l)});break;case q.Literal:r=this.escape(a.value);this.assign(c,r);e(r);break;case q.UnaryExpression:this.recurse(a.argument,t,t,function(a){l=a});r=a.operator+"("+this.ifDefined(l,0)+")";this.assign(c,r);e(r);break;case q.BinaryExpression:this.recurse(a.left,
214 t,t,function(a){h=a});this.recurse(a.right,t,t,function(a){l=a});r="+"===a.operator?this.plus(h,l):"-"===a.operator?this.ifDefined(h,0)+a.operator+this.ifDefined(l,0):"("+h+")"+a.operator+"("+l+")";this.assign(c,r);e(r);break;case q.LogicalExpression:c=c||this.nextId();k.recurse(a.left,c);k.if_("&&"===a.operator?c:k.not(c),k.lazyRecurse(a.right,c));e(c);break;case q.ConditionalExpression:c=c||this.nextId();k.recurse(a.test,c);k.if_(c,k.lazyRecurse(a.alternate,c),k.lazyRecurse(a.consequent,c));e(c);
215 break;case q.Identifier:c=c||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);Ca(a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){f&&1!==f&&k.if_(k.not(k.nonComputedMember("s",a.name)),k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(c,k.nonComputedMember("s",a.name))})},c&&k.lazyAssign(c,k.nonComputedMember("l",
216 a.name)));(k.state.expensiveChecks||Fb(a.name))&&k.addEnsureSafeObject(c);e(c);break;case q.MemberExpression:h=d&&(d.context=this.nextId())||this.nextId();c=c||this.nextId();k.recurse(a.object,h,t,function(){k.if_(k.notNull(h),function(){if(a.computed)l=k.nextId(),k.recurse(a.property,l),k.addEnsureSafeMemberName(l),f&&1!==f&&k.if_(k.not(k.computedMember(h,l)),k.lazyAssign(k.computedMember(h,l),"{}")),r=k.ensureSafeObject(k.computedMember(h,l)),k.assign(c,r),d&&(d.computed=!0,d.name=l);else{Ca(a.property.name);
217 f&&1!==f&&k.if_(k.not(k.nonComputedMember(h,a.property.name)),k.lazyAssign(k.nonComputedMember(h,a.property.name),"{}"));r=k.nonComputedMember(h,a.property.name);if(k.state.expensiveChecks||Fb(a.property.name))r=k.ensureSafeObject(r);k.assign(c,r);d&&(d.computed=!1,d.name=a.property.name)}},function(){k.assign(c,"undefined")});e(c)},!!f);break;case q.CallExpression:c=c||this.nextId();a.filter?(l=k.filter(a.callee.name),n=[],m(a.arguments,function(a){var c=k.nextId();k.recurse(a,c);n.push(c)}),r=l+
218 "("+n.join(",")+")",k.assign(c,r),e(c)):(l=k.nextId(),h={},n=[],k.recurse(a.callee,l,h,function(){k.if_(k.notNull(l),function(){k.addEnsureSafeFunction(l);m(a.arguments,function(a){k.recurse(a,k.nextId(),t,function(a){n.push(k.ensureSafeObject(a))})});h.name?(k.state.expensiveChecks||k.addEnsureSafeObject(h.context),r=k.member(h.context,h.name,h.computed)+"("+n.join(",")+")"):r=l+"("+n.join(",")+")";r=k.ensureSafeObject(r);k.assign(c,r)},function(){k.assign(c,"undefined")});e(c)}));break;case q.AssignmentExpression:l=
219 this.nextId();h={};if(!od(a.left))throw da("lval");this.recurse(a.left,t,h,function(){k.if_(k.notNull(h.context),function(){k.recurse(a.right,l);k.addEnsureSafeObject(k.member(h.context,h.name,h.computed));r=k.member(h.context,h.name,h.computed)+a.operator+l;k.assign(c,r);e(c||r)})},1);break;case q.ArrayExpression:n=[];m(a.elements,function(a){k.recurse(a,k.nextId(),t,function(a){n.push(a)})});r="["+n.join(",")+"]";this.assign(c,r);e(r);break;case q.ObjectExpression:n=[];m(a.properties,function(a){k.recurse(a.value,
220 k.nextId(),t,function(c){n.push(k.escape(a.key.type===q.Identifier?a.key.name:""+a.key.value)+":"+c)})});r="{"+n.join(",")+"}";this.assign(c,r);e(r);break;case q.ThisExpression:this.assign(c,"s");e("s");break;case q.NGValueParameter:this.assign(c,"v"),e("v")}},getHasOwnProperty:function(a,c){var d=a+"."+c,e=this.current().own;e.hasOwnProperty(d)||(e[d]=this.nextId(!1,a+"&&("+this.escape(c)+" in "+a+")"));return e[d]},assign:function(a,c){if(a)return this.current().body.push(a,"=",c,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||
221 (this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,c){return"ifDefined("+a+","+this.escape(c)+")"},plus:function(a,c){return"plus("+a+","+c+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a,c,d){if(!0===a)c();else{var e=this.current().body;e.push("if(",a,"){");c();e.push("}");d&&(e.push("else{"),d(),e.push("}"))}},not:function(a){return"!("+a+")"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,c){return a+
222 "."+c},computedMember:function(a,c){return a+"["+c+"]"},member:function(a,c,d){return d?this.computedMember(a,c):this.nonComputedMember(a,c)},addEnsureSafeObject:function(a){this.current().body.push(this.ensureSafeObject(a),";")},addEnsureSafeMemberName:function(a){this.current().body.push(this.ensureSafeMemberName(a),";")},addEnsureSafeFunction:function(a){this.current().body.push(this.ensureSafeFunction(a),";")},ensureSafeObject:function(a){return"ensureSafeObject("+a+",text)"},ensureSafeMemberName:function(a){return"ensureSafeMemberName("+
223 a+",text)"},ensureSafeFunction:function(a){return"ensureSafeFunction("+a+",text)"},lazyRecurse:function(a,c,d,e,f,g){var h=this;return function(){h.recurse(a,c,d,e,f,g)}},lazyAssign:function(a,c){var d=this;return function(){d.assign(a,c)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(L(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(V(a))return a.toString();if(!0===a)return"true";
224 if(!1===a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw da("esc");},nextId:function(a,c){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(c?"="+c:""));return d},current:function(){return this.state[this.state.computing]}};sd.prototype={compile:function(a,c){var d=this,e=this.astBuilder.ast(a);this.expression=a;this.expensiveChecks=c;T(e,d.$filter);var f,g;if(f=pd(e))g=this.recurse(f);f=nd(e.body);var h;f&&(h=[],m(f,function(a,c){var e=d.recurse(a);
225 a.input=e;h.push(e);a.watchId=c}));var l=[];m(e.body,function(a){l.push(d.recurse(a.expression))});f=0===e.body.length?function(){}:1===e.body.length?l[0]:function(a,c){var d;m(l,function(e){d=e(a,c)});return d};g&&(f.assign=function(a,c,d){return g(a,d,c)});h&&(f.inputs=h);f.literal=qd(e);f.constant=e.constant;return f},recurse:function(a,c,d){var e,f,g=this,h;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case q.Literal:return this.value(a.value,c);case q.UnaryExpression:return f=
226 this.recurse(a.argument),this["unary"+a.operator](f,c);case q.BinaryExpression:return e=this.recurse(a.left),f=this.recurse(a.right),this["binary"+a.operator](e,f,c);case q.LogicalExpression:return e=this.recurse(a.left),f=this.recurse(a.right),this["binary"+a.operator](e,f,c);case q.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),c);case q.Identifier:return Ca(a.name,g.expression),g.identifier(a.name,g.expensiveChecks||Fb(a.name),
227 c,d,g.expression);case q.MemberExpression:return e=this.recurse(a.object,!1,!!d),a.computed||(Ca(a.property.name,g.expression),f=a.property.name),a.computed&&(f=this.recurse(a.property)),a.computed?this.computedMember(e,f,c,d,g.expression):this.nonComputedMember(e,f,g.expensiveChecks,c,d,g.expression);case q.CallExpression:return h=[],m(a.arguments,function(a){h.push(g.recurse(a))}),a.filter&&(f=this.$filter(a.callee.name)),a.filter||(f=this.recurse(a.callee,!0)),a.filter?function(a,d,e,g){for(var m=
228 [],q=0;q<h.length;++q)m.push(h[q](a,d,e,g));a=f.apply(t,m,g);return c?{context:t,name:t,value:a}:a}:function(a,d,e,r){var m=f(a,d,e,r),q;if(null!=m.value){oa(m.context,g.expression);ld(m.value,g.expression);q=[];for(var t=0;t<h.length;++t)q.push(oa(h[t](a,d,e,r),g.expression));q=oa(m.value.apply(m.context,q),g.expression)}return c?{value:q}:q};case q.AssignmentExpression:return e=this.recurse(a.left,!0,1),f=this.recurse(a.right),function(a,d,h,r){var m=e(a,d,h,r);a=f(a,d,h,r);oa(m.value,g.expression);
229 m.context[m.name]=a;return c?{value:a}:a};case q.ArrayExpression:return h=[],m(a.elements,function(a){h.push(g.recurse(a))}),function(a,d,e,f){for(var g=[],m=0;m<h.length;++m)g.push(h[m](a,d,e,f));return c?{value:g}:g};case q.ObjectExpression:return h=[],m(a.properties,function(a){h.push({key:a.key.type===q.Identifier?a.key.name:""+a.key.value,value:g.recurse(a.value)})}),function(a,d,e,f){for(var g={},m=0;m<h.length;++m)g[h[m].key]=h[m].value(a,d,e,f);return c?{value:g}:g};case q.ThisExpression:return function(a){return c?
230 {value:a}:a};case q.NGValueParameter:return function(a,d,e,f){return c?{value:e}:e}}},"unary+":function(a,c){return function(d,e,f,g){d=a(d,e,f,g);d=w(d)?+d:0;return c?{value:d}:d}},"unary-":function(a,c){return function(d,e,f,g){d=a(d,e,f,g);d=w(d)?-d:0;return c?{value:d}:d}},"unary!":function(a,c){return function(d,e,f,g){d=!a(d,e,f,g);return c?{value:d}:d}},"binary+":function(a,c,d){return function(e,f,g,h){var l=a(e,f,g,h);e=c(e,f,g,h);l=md(l,e);return d?{value:l}:l}},"binary-":function(a,c,d){return function(e,
231 f,g,h){var l=a(e,f,g,h);e=c(e,f,g,h);l=(w(l)?l:0)-(w(e)?e:0);return d?{value:l}:l}},"binary*":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)*c(e,f,g,h);return d?{value:e}:e}},"binary/":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)/c(e,f,g,h);return d?{value:e}:e}},"binary%":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)%c(e,f,g,h);return d?{value:e}:e}},"binary===":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)===c(e,f,g,h);return d?{value:e}:e}},"binary!==":function(a,
232 c,d){return function(e,f,g,h){e=a(e,f,g,h)!==c(e,f,g,h);return d?{value:e}:e}},"binary==":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)==c(e,f,g,h);return d?{value:e}:e}},"binary!=":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)!=c(e,f,g,h);return d?{value:e}:e}},"binary<":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)<c(e,f,g,h);return d?{value:e}:e}},"binary>":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)>c(e,f,g,h);return d?{value:e}:e}},"binary<=":function(a,c,d){return function(e,
233 f,g,h){e=a(e,f,g,h)<=c(e,f,g,h);return d?{value:e}:e}},"binary>=":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)>=c(e,f,g,h);return d?{value:e}:e}},"binary&&":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)&&c(e,f,g,h);return d?{value:e}:e}},"binary||":function(a,c,d){return function(e,f,g,h){e=a(e,f,g,h)||c(e,f,g,h);return d?{value:e}:e}},"ternary?:":function(a,c,d,e){return function(f,g,h,l){f=a(f,g,h,l)?c(f,g,h,l):d(f,g,h,l);return e?{value:f}:f}},value:function(a,c){return function(){return c?
234 {context:t,name:t,value:a}:a}},identifier:function(a,c,d,e,f){return function(g,h,l,k){g=h&&a in h?h:g;e&&1!==e&&g&&!g[a]&&(g[a]={});h=g?g[a]:t;c&&oa(h,f);return d?{context:g,name:a,value:h}:h}},computedMember:function(a,c,d,e,f){return function(g,h,l,k){var n=a(g,h,l,k),m,s;null!=n&&(m=c(g,h,l,k),Ca(m,f),e&&1!==e&&n&&!n[m]&&(n[m]={}),s=n[m],oa(s,f));return d?{context:n,name:m,value:s}:s}},nonComputedMember:function(a,c,d,e,f,g){return function(h,l,k,n){h=a(h,l,k,n);f&&1!==f&&h&&!h[c]&&(h[c]={});
235 l=null!=h?h[c]:t;(d||Fb(c))&&oa(l,g);return e?{context:h,name:c,value:l}:l}},inputs:function(a,c){return function(d,e,f,g){return g?g[c]:a(d,e,f)}}};var hc=function(a,c,d){this.lexer=a;this.$filter=c;this.options=d;this.ast=new q(this.lexer);this.astCompiler=d.csp?new sd(this.ast,c):new rd(this.ast,c)};hc.prototype={constructor:hc,parse:function(a){return this.astCompiler.compile(a,this.options.expensiveChecks)}};ga();ga();var Yf=Object.prototype.valueOf,Da=J("$sce"),pa={HTML:"html",CSS:"css",URL:"url",
236 RESOURCE_URL:"resourceUrl",JS:"js"},ea=J("$compile"),X=U.createElement("a"),wd=Ba(O.location.href);xd.$inject=["$document"];Lc.$inject=["$provide"];yd.$inject=["$locale"];Ad.$inject=["$locale"];var Dd=".",hg={yyyy:Y("FullYear",4),yy:Y("FullYear",2,0,!0),y:Y("FullYear",1),MMMM:Hb("Month"),MMM:Hb("Month",!0),MM:Y("Month",2,1),M:Y("Month",1,1),dd:Y("Date",2),d:Y("Date",1),HH:Y("Hours",2),H:Y("Hours",1),hh:Y("Hours",2,-12),h:Y("Hours",1,-12),mm:Y("Minutes",2),m:Y("Minutes",1),ss:Y("Seconds",2),s:Y("Seconds",
237 1),sss:Y("Milliseconds",3),EEEE:Hb("Day"),EEE:Hb("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a,c,d){a=-1*d;return a=(0<=a?"+":"")+(Gb(Math[0<a?"floor":"ceil"](a/60),2)+Gb(Math.abs(a%60),2))},ww:Fd(2),w:Fd(1),G:jc,GG:jc,GGG:jc,GGGG:function(a,c){return 0>=a.getFullYear()?c.ERANAMES[0]:c.ERANAMES[1]}},gg=/((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,fg=/^\-?\d+$/;zd.$inject=["$locale"];var cg=ra(M),dg=ra(rb);Bd.$inject=
238 ["$parse"];var ie=ra({restrict:"E",compile:function(a,c){if(!c.href&&!c.xlinkHref)return function(a,c){if("a"===c[0].nodeName.toLowerCase()){var f="[object SVGAnimatedString]"===sa.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(f)||a.preventDefault()})}}}}),sb={};m(Ab,function(a,c){function d(a,d,f){a.$watch(f[e],function(a){f.$set(c,!!a)})}if("multiple"!=a){var e=wa("ng-"+c),f=d;"checked"===a&&(f=function(a,c,f){f.ngModel!==f[e]&&d(a,c,f)});sb[e]=function(){return{restrict:"A",
239 priority:100,link:f}}}});m(Uc,function(a,c){sb[c]=function(){return{priority:100,link:function(a,e,f){if("ngPattern"===c&&"/"==f.ngPattern.charAt(0)&&(e=f.ngPattern.match(jg))){f.$set("ngPattern",new RegExp(e[1],e[2]));return}a.$watch(f[c],function(a){f.$set(c,a)})}}}});m(["src","srcset","href"],function(a){var c=wa("ng-"+a);sb[c]=function(){return{priority:99,link:function(d,e,f){var g=a,h=a;"href"===a&&"[object SVGAnimatedString]"===sa.call(e.prop("href"))&&(h="xlinkHref",f.$attr[h]="xlink:href",
240 g=null);f.$observe(c,function(c){c?(f.$set(h,c),Ua&&g&&e.prop(g,f[h])):"href"===a&&f.$set(h,null)})}}}});var Ib={$addControl:v,$$renameControl:function(a,c){a.$name=c},$removeControl:v,$setValidity:v,$setDirty:v,$setPristine:v,$setSubmitted:v};Gd.$inject=["$element","$attrs","$scope","$animate","$interpolate"];var Od=function(a){return["$timeout",function(c){return{name:"form",restrict:a?"EAC":"E",controller:Gd,compile:function(d,e){d.addClass(Va).addClass(mb);var f=e.name?"name":a&&e.ngForm?"ngForm":
241 !1;return{pre:function(a,d,e,k){if(!("action"in e)){var n=function(c){a.$apply(function(){k.$commitViewValue();k.$setSubmitted()});c.preventDefault()};d[0].addEventListener("submit",n,!1);d.on("$destroy",function(){c(function(){d[0].removeEventListener("submit",n,!1)},0,!1)})}var m=k.$$parentForm;f&&(Eb(a,k.$name,k,k.$name),e.$observe(f,function(c){k.$name!==c&&(Eb(a,k.$name,t,k.$name),m.$$renameControl(k,c),Eb(a,k.$name,k,k.$name))}));d.on("$destroy",function(){m.$removeControl(k);f&&Eb(a,e[f],t,
242 k.$name);P(k,Ib)})}}}}}]},je=Od(),we=Od(!0),ig=/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/,rg=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/,sg=/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i,tg=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,Pd=/^(\d{4})-(\d{2})-(\d{2})$/,Qd=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,mc=/^(\d{4})-W(\d\d)$/,Rd=/^(\d{4})-(\d\d)$/,
243 Sd=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Td={text:function(a,c,d,e,f,g){kb(a,c,d,e,f,g);kc(e)},date:lb("date",Pd,Kb(Pd,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":lb("datetimelocal",Qd,Kb(Qd,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:lb("time",Sd,Kb(Sd,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:lb("week",mc,function(a,c){if(aa(a))return a;if(L(a)){mc.lastIndex=0;var d=mc.exec(a);if(d){var e=+d[1],f=+d[2],g=d=0,h=0,l=0,k=Ed(e),f=7*(f-1);c&&(d=c.getHours(),g=
244 c.getMinutes(),h=c.getSeconds(),l=c.getMilliseconds());return new Date(e,0,k.getDate()+f,d,g,h,l)}}return NaN},"yyyy-Www"),month:lb("month",Rd,Kb(Rd,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,f,g){Id(a,c,d,e);kb(a,c,d,e,f,g);e.$$parserName="number";e.$parsers.push(function(a){return e.$isEmpty(a)?null:tg.test(a)?parseFloat(a):t});e.$formatters.push(function(a){if(!e.$isEmpty(a)){if(!V(a))throw Lb("numfmt",a);a=a.toString()}return a});if(w(d.min)||d.ngMin){var h;e.$validators.min=function(a){return e.$isEmpty(a)||
245 A(h)||a>=h};d.$observe("min",function(a){w(a)&&!V(a)&&(a=parseFloat(a,10));h=V(a)&&!isNaN(a)?a:t;e.$validate()})}if(w(d.max)||d.ngMax){var l;e.$validators.max=function(a){return e.$isEmpty(a)||A(l)||a<=l};d.$observe("max",function(a){w(a)&&!V(a)&&(a=parseFloat(a,10));l=V(a)&&!isNaN(a)?a:t;e.$validate()})}},url:function(a,c,d,e,f,g){kb(a,c,d,e,f,g);kc(e);e.$$parserName="url";e.$validators.url=function(a,c){var d=a||c;return e.$isEmpty(d)||rg.test(d)}},email:function(a,c,d,e,f,g){kb(a,c,d,e,f,g);kc(e);
246 e.$$parserName="email";e.$validators.email=function(a,c){var d=a||c;return e.$isEmpty(d)||sg.test(d)}},radio:function(a,c,d,e){A(d.name)&&c.attr("name",++nb);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,l){var k=Jd(l,a,"ngTrueValue",d.ngTrueValue,!0),n=Jd(l,a,"ngFalseValue",d.ngFalseValue,!1);c.on("click",function(a){e.$setViewValue(c[0].checked,a&&
247 a.type)});e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return!1===a};e.$formatters.push(function(a){return ka(a,k)});e.$parsers.push(function(a){return a?k:n})},hidden:v,button:v,submit:v,reset:v,file:v},Fc=["$browser","$sniffer","$filter","$parse",function(a,c,d,e){return{restrict:"E",require:["?ngModel"],link:{pre:function(f,g,h,l){l[0]&&(Td[M(h.type)]||Td.text)(f,g,h,l[0],c,a,d,e)}}}}],ug=/^(true|false|\d+)$/,Oe=function(){return{restrict:"A",priority:100,compile:function(a,
248 c){return ug.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)})}}}},oe=["$compile",function(a){return{restrict:"AC",compile:function(c){a.$$addBindingClass(c);return function(c,e,f){a.$$addBindingInfo(e,f.ngBind);e=e[0];c.$watch(f.ngBind,function(a){e.textContent=a===t?"":a})}}}}],qe=["$interpolate","$compile",function(a,c){return{compile:function(d){c.$$addBindingClass(d);return function(d,f,g){d=a(f.attr(g.$attr.ngBindTemplate));
249 c.$$addBindingInfo(f,d.expressions);f=f[0];g.$observe("ngBindTemplate",function(a){f.textContent=a===t?"":a})}}}}],pe=["$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))||"")})}}}}],Ne=ra({restrict:"A",require:"ngModel",link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),
250 re=lc("",!0),te=lc("Odd",0),se=lc("Even",1),ue=Ma({compile:function(a,c){c.$set("ngCloak",t);a.removeClass("ng-cloak")}}),ve=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],Kc={},vg={blur:!0,focus:!0};m("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=wa("ng-"+a);Kc[c]=["$parse","$rootScope",function(d,e){return{restrict:"A",compile:function(f,g){var h=
251 d(g[c],null,!0);return function(c,d){d.on(a,function(d){var f=function(){h(c,{$event:d})};vg[a]&&e.$$phase?c.$evalAsync(f):c.$apply(f)})}}}}]});var ye=["$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,l,k;c.$watch(e.ngIf,function(c){c?l||g(function(c,f){l=f;c[c.length++]=U.createComment(" end ngIf: "+e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)}):(k&&(k.remove(),k=null),l&&(l.$destroy(),l=null),h&&(k=
252 qb(h.clone),a.leave(k).then(function(){k=null}),h=null))})}}}],ze=["$templateRequest","$anchorScroll","$animate",function(a,c,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ca.noop,compile:function(e,f){var g=f.ngInclude||f.src,h=f.onload||"",l=f.autoscroll;return function(e,f,m,s,q){var t=0,F,u,p,v=function(){u&&(u.remove(),u=null);F&&(F.$destroy(),F=null);p&&(d.leave(p).then(function(){u=null}),u=p,p=null)};e.$watch(g,function(g){var m=function(){!w(l)||l&&!e.$eval(l)||
253 c()},r=++t;g?(a(g,!0).then(function(a){if(r===t){var c=e.$new();s.template=a;a=q(c,function(a){v();d.enter(a,null,f).then(m)});F=c;p=a;F.$emit("$includeContentLoaded",g);e.$eval(h)}},function(){r===t&&(v(),e.$emit("$includeContentError",g))}),e.$emit("$includeContentRequested",g)):(v(),s.template=null)})}}}}],Qe=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,f){/SVG/.test(d[0].toString())?(d.empty(),a(Nc(f.template,U).childNodes)(c,function(a){d.append(a)},
254 {futureParentElement:d})):(d.html(f.template),a(d.contents())(c))}}}],Ae=Ma({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Me=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?R(f):f;e.$parsers.push(function(a){if(!A(a)){var c=[];a&&m(a.split(h),function(a){a&&c.push(g?R(a):a)});return c}});e.$formatters.push(function(a){return G(a)?a.join(f):t});e.$isEmpty=function(a){return!a||
255 !a.length}}}},mb="ng-valid",Kd="ng-invalid",Va="ng-pristine",Jb="ng-dirty",Md="ng-pending",Lb=new J("ngModel"),wg=["$scope","$exceptionHandler","$attrs","$element","$parse","$animate","$timeout","$rootScope","$q","$interpolate",function(a,c,d,e,f,g,h,l,k,n){this.$modelValue=this.$viewValue=Number.NaN;this.$$rawModelValue=t;this.$validators={};this.$asyncValidators={};this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$untouched=!0;this.$touched=!1;this.$pristine=!0;this.$dirty=
256 !1;this.$valid=!0;this.$invalid=!1;this.$error={};this.$$success={};this.$pending=t;this.$name=n(d.name||"",!1)(a);var r=f(d.ngModel),s=r.assign,q=r,C=s,F=null,u,p=this;this.$$setOptions=function(a){if((p.$options=a)&&a.getterSetter){var c=f(d.ngModel+"()"),g=f(d.ngModel+"($$$p)");q=function(a){var d=r(a);z(d)&&(d=c(a));return d};C=function(a,c){z(r(a))?g(a,{$$$p:p.$modelValue}):s(a,p.$modelValue)}}else if(!r.assign)throw Lb("nonassign",d.ngModel,ua(e));};this.$render=v;this.$isEmpty=function(a){return A(a)||
257 ""===a||null===a||a!==a};var K=e.inheritedData("$formController")||Ib,y=0;Hd({ctrl:this,$element:e,set:function(a,c){a[c]=!0},unset:function(a,c){delete a[c]},parentForm:K,$animate:g});this.$setPristine=function(){p.$dirty=!1;p.$pristine=!0;g.removeClass(e,Jb);g.addClass(e,Va)};this.$setDirty=function(){p.$dirty=!0;p.$pristine=!1;g.removeClass(e,Va);g.addClass(e,Jb);K.$setDirty()};this.$setUntouched=function(){p.$touched=!1;p.$untouched=!0;g.setClass(e,"ng-untouched","ng-touched")};this.$setTouched=
258 function(){p.$touched=!0;p.$untouched=!1;g.setClass(e,"ng-touched","ng-untouched")};this.$rollbackViewValue=function(){h.cancel(F);p.$viewValue=p.$$lastCommittedViewValue;p.$render()};this.$validate=function(){if(!V(p.$modelValue)||!isNaN(p.$modelValue)){var a=p.$$rawModelValue,c=p.$valid,d=p.$modelValue,e=p.$options&&p.$options.allowInvalid;p.$$runValidators(a,p.$$lastCommittedViewValue,function(f){e||c===f||(p.$modelValue=f?a:t,p.$modelValue!==d&&p.$$writeModelToScope())})}};this.$$runValidators=
259 function(a,c,d){function e(){var d=!0;m(p.$validators,function(e,f){var h=e(a,c);d=d&&h;g(f,h)});return d?!0:(m(p.$asyncValidators,function(a,c){g(c,null)}),!1)}function f(){var d=[],e=!0;m(p.$asyncValidators,function(f,h){var k=f(a,c);if(!k||!z(k.then))throw Lb("$asyncValidators",k);g(h,t);d.push(k.then(function(){g(h,!0)},function(a){e=!1;g(h,!1)}))});d.length?k.all(d).then(function(){h(e)},v):h(!0)}function g(a,c){l===y&&p.$setValidity(a,c)}function h(a){l===y&&d(a)}y++;var l=y;(function(){var a=
260 p.$$parserName||"parse";if(u===t)g(a,null);else return u||(m(p.$validators,function(a,c){g(c,null)}),m(p.$asyncValidators,function(a,c){g(c,null)})),g(a,u),u;return!0})()?e()?f():h(!1):h(!1)};this.$commitViewValue=function(){var a=p.$viewValue;h.cancel(F);if(p.$$lastCommittedViewValue!==a||""===a&&p.$$hasNativeValidators)p.$$lastCommittedViewValue=a,p.$pristine&&this.$setDirty(),this.$$parseAndValidate()};this.$$parseAndValidate=function(){var c=p.$$lastCommittedViewValue;if(u=A(c)?t:!0)for(var d=
261 0;d<p.$parsers.length;d++)if(c=p.$parsers[d](c),A(c)){u=!1;break}V(p.$modelValue)&&isNaN(p.$modelValue)&&(p.$modelValue=q(a));var e=p.$modelValue,f=p.$options&&p.$options.allowInvalid;p.$$rawModelValue=c;f&&(p.$modelValue=c,p.$modelValue!==e&&p.$$writeModelToScope());p.$$runValidators(c,p.$$lastCommittedViewValue,function(a){f||(p.$modelValue=a?c:t,p.$modelValue!==e&&p.$$writeModelToScope())})};this.$$writeModelToScope=function(){C(a,p.$modelValue);m(p.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}})};
262 this.$setViewValue=function(a,c){p.$viewValue=a;p.$options&&!p.$options.updateOnDefault||p.$$debounceViewValueCommit(c)};this.$$debounceViewValueCommit=function(c){var d=0,e=p.$options;e&&w(e.debounce)&&(e=e.debounce,V(e)?d=e:V(e[c])?d=e[c]:V(e["default"])&&(d=e["default"]));h.cancel(F);d?F=h(function(){p.$commitViewValue()},d):l.$$phase?p.$commitViewValue():a.$apply(function(){p.$commitViewValue()})};a.$watch(function(){var c=q(a);if(c!==p.$modelValue&&(p.$modelValue===p.$modelValue||c===c)){p.$modelValue=
263 p.$$rawModelValue=c;u=t;for(var d=p.$formatters,e=d.length,f=c;e--;)f=d[e](f);p.$viewValue!==f&&(p.$viewValue=p.$$lastCommittedViewValue=f,p.$render(),p.$$runValidators(c,f,v))}return c})}],Le=["$rootScope",function(a){return{restrict:"A",require:["ngModel","^?form","^?ngModelOptions"],controller:wg,priority:1,compile:function(c){c.addClass(Va).addClass("ng-untouched").addClass(mb);return{pre:function(a,c,f,g){var h=g[0],l=g[1]||Ib;h.$$setOptions(g[2]&&g[2].$options);l.$addControl(h);f.$observe("name",
264 function(a){h.$name!==a&&l.$$renameControl(h,a)});a.$on("$destroy",function(){l.$removeControl(h)})},post:function(c,e,f,g){var h=g[0];if(h.$options&&h.$options.updateOn)e.on(h.$options.updateOn,function(a){h.$$debounceViewValueCommit(a&&a.type)});e.on("blur",function(e){h.$touched||(a.$$phase?c.$evalAsync(h.$setTouched):c.$apply(h.$setTouched))})}}}}}],xg=/(\s+|^)default(\s+|$)/,Pe=function(){return{restrict:"A",controller:["$scope","$attrs",function(a,c){var d=this;this.$options=fa(a.$eval(c.ngModelOptions));
265 this.$options.updateOn!==t?(this.$options.updateOnDefault=!1,this.$options.updateOn=R(this.$options.updateOn.replace(xg,function(){d.$options.updateOnDefault=!0;return" "}))):this.$options.updateOnDefault=!0}]}},Be=Ma({terminal:!0,priority:1E3}),yg=J("ngOptions"),zg=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
266 Je=["$compile","$parse",function(a,c){function d(a,d,e){function f(a,c,d,e,g){this.selectValue=a;this.viewValue=c;this.label=d;this.group=e;this.disabled=g}function n(a){var c;if(!q&&Ea(a))c=a;else{c=[];for(var d in a)a.hasOwnProperty(d)&&"$"!==d.charAt(0)&&c.push(d)}return c}var m=a.match(zg);if(!m)throw yg("iexp",a,ua(d));var s=m[5]||m[7],q=m[6];a=/ as /.test(m[0])&&m[1];var t=m[9];d=c(m[2]?m[1]:s);var v=a&&c(a)||d,u=t&&c(t),p=t?function(a,c){return u(e,c)}:function(a){return Ga(a)},w=function(a,
267 c){return p(a,z(a,c))},y=c(m[2]||m[1]),A=c(m[3]||""),B=c(m[4]||""),N=c(m[8]),D={},z=q?function(a,c){D[q]=c;D[s]=a;return D}:function(a){D[s]=a;return D};return{trackBy:t,getTrackByValue:w,getWatchables:c(N,function(a){var c=[];a=a||[];for(var d=n(a),f=d.length,g=0;g<f;g++){var h=a===d?g:d[g],k=z(a[h],h),h=p(a[h],k);c.push(h);if(m[2]||m[1])h=y(e,k),c.push(h);m[4]&&(k=B(e,k),c.push(k))}return c}),getOptions:function(){for(var a=[],c={},d=N(e)||[],g=n(d),h=g.length,m=0;m<h;m++){var r=d===g?m:g[m],s=
268 z(d[r],r),q=v(e,s),r=p(q,s),u=y(e,s),x=A(e,s),s=B(e,s),q=new f(r,q,u,x,s);a.push(q);c[r]=q}return{items:a,selectValueMap:c,getOptionFromViewValue:function(a){return c[w(a)]},getViewValueFromOption:function(a){return t?ca.copy(a.viewValue):a.viewValue}}}}}var e=U.createElement("option"),f=U.createElement("optgroup");return{restrict:"A",terminal:!0,require:["select","?ngModel"],link:function(c,h,l,k){function n(a,c){a.element=c;c.disabled=a.disabled;a.value!==c.value&&(c.value=a.selectValue);a.label!==
269 c.label&&(c.label=a.label,c.textContent=a.label)}function r(a,c,d,e){c&&M(c.nodeName)===d?d=c:(d=e.cloneNode(!1),c?a.insertBefore(d,c):a.appendChild(d));return d}function s(a){for(var c;a;)c=a.nextSibling,Xb(a),a=c}function q(a){var c=p&&p[0],d=N&&N[0];if(c||d)for(;a&&(a===c||a===d);)a=a.nextSibling;return a}function t(){var a=D&&u.readValue();D=z.getOptions();var c={},d=h[0].firstChild;B&&h.prepend(p);d=q(d);D.items.forEach(function(a){var g,k;a.group?(g=c[a.group],g||(g=r(h[0],d,"optgroup",f),d=
270 g.nextSibling,g.label=a.group,g=c[a.group]={groupElement:g,currentOptionElement:g.firstChild}),k=r(g.groupElement,g.currentOptionElement,"option",e),n(a,k),g.currentOptionElement=k.nextSibling):(k=r(h[0],d,"option",e),n(a,k),d=k.nextSibling)});Object.keys(c).forEach(function(a){s(c[a].currentOptionElement)});s(d);v.$render();if(!v.$isEmpty(a)){var g=u.readValue();(z.trackBy?ka(a,g):a===g)||(v.$setViewValue(g),v.$render())}}var v=k[1];if(v){var u=k[0];k=l.multiple;for(var p,w=0,A=h.children(),I=A.length;w<
271 I;w++)if(""===A[w].value){p=A.eq(w);break}var B=!!p,N=y(e.cloneNode(!1));N.val("?");var D,z=d(l.ngOptions,h,c);k?(v.$isEmpty=function(a){return!a||0===a.length},u.writeValue=function(a){D.items.forEach(function(a){a.element.selected=!1});a&&a.forEach(function(a){(a=D.getOptionFromViewValue(a))&&!a.disabled&&(a.element.selected=!0)})},u.readValue=function(){var a=h.val()||[],c=[];m(a,function(a){a=D.selectValueMap[a];a.disabled||c.push(D.getViewValueFromOption(a))});return c},z.trackBy&&c.$watchCollection(function(){if(G(v.$viewValue))return v.$viewValue.map(function(a){return z.getTrackByValue(a)})},
272 function(){v.$render()})):(u.writeValue=function(a){var c=D.getOptionFromViewValue(a);c&&!c.disabled?h[0].value!==c.selectValue&&(N.remove(),B||p.remove(),h[0].value=c.selectValue,c.element.selected=!0,c.element.setAttribute("selected","selected")):null===a||B?(N.remove(),B||h.prepend(p),h.val(""),p.prop("selected",!0),p.attr("selected",!0)):(B||p.remove(),h.prepend(N),h.val("?"),N.prop("selected",!0),N.attr("selected",!0))},u.readValue=function(){var a=D.selectValueMap[h.val()];return a&&!a.disabled?
273 (B||p.remove(),N.remove(),D.getViewValueFromOption(a)):null},z.trackBy&&c.$watch(function(){return z.getTrackByValue(v.$viewValue)},function(){v.$render()}));B?(p.remove(),a(p)(c),p.removeClass("ng-scope")):p=y(e.cloneNode(!1));t();c.$watchCollection(z.getWatchables,t)}}}}],Ce=["$locale","$interpolate","$log",function(a,c,d){var e=/{}/g,f=/^when(Minus)?(.+)$/;return{link:function(g,h,l){function k(a){h.text(a||"")}var n=l.count,r=l.$attr.when&&h.attr(l.$attr.when),s=l.offset||0,q=g.$eval(r)||{},t=
274 {},w=c.startSymbol(),u=c.endSymbol(),p=w+n+"-"+s+u,y=ca.noop,z;m(l,function(a,c){var d=f.exec(c);d&&(d=(d[1]?"-":"")+M(d[2]),q[d]=h.attr(l.$attr[c]))});m(q,function(a,d){t[d]=c(a.replace(e,p))});g.$watch(n,function(c){var e=parseFloat(c),f=isNaN(e);f||e in q||(e=a.pluralCat(e-s));e===z||f&&V(z)&&isNaN(z)||(y(),f=t[e],A(f)?(null!=c&&d.debug("ngPluralize: no rule defined for '"+e+"' in "+r),y=v,k()):y=g.$watch(f,k),z=e)})}}}],De=["$parse","$animate",function(a,c){var d=J("ngRepeat"),e=function(a,c,
275 d,e,k,m,r){a[d]=e;k&&(a[k]=m);a.$index=c;a.$first=0===c;a.$last=c===r-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,l=U.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],r=k[2],s=k[3],q=k[4],k=n.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);
276 if(!k)throw d("iidexp",n);var v=k[3]||k[1],w=k[2];if(s&&(!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(s)||/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(s)))throw d("badident",s);var u,p,z,A,I={$id:Ga};q?u=a(q):(z=function(a,c){return Ga(c)},A=function(a){return a});return function(a,f,g,k,n){u&&(p=function(c,d,e){w&&(I[w]=c);I[v]=d;I.$index=e;return u(a,I)});var q=ga();a.$watchCollection(r,function(g){var k,r,u=f[0],x,D=ga(),I,H,L,G,M,J,O;s&&(a[s]=g);if(Ea(g))M=
277 g,r=p||z;else for(O in r=p||A,M=[],g)g.hasOwnProperty(O)&&"$"!==O.charAt(0)&&M.push(O);I=M.length;O=Array(I);for(k=0;k<I;k++)if(H=g===M?k:M[k],L=g[H],G=r(H,L,k),q[G])J=q[G],delete q[G],D[G]=J,O[k]=J;else{if(D[G])throw m(O,function(a){a&&a.scope&&(q[a.id]=a)}),d("dupes",h,G,L);O[k]={id:G,scope:t,clone:t};D[G]=!0}for(x in q){J=q[x];G=qb(J.clone);c.leave(G);if(G[0].parentNode)for(k=0,r=G.length;k<r;k++)G[k].$$NG_REMOVED=!0;J.scope.$destroy()}for(k=0;k<I;k++)if(H=g===M?k:M[k],L=g[H],J=O[k],J.scope){x=
278 u;do x=x.nextSibling;while(x&&x.$$NG_REMOVED);J.clone[0]!=x&&c.move(qb(J.clone),null,y(u));u=J.clone[J.clone.length-1];e(J.scope,k,v,L,w,H,I)}else n(function(a,d){J.scope=d;var f=l.cloneNode(!1);a[a.length++]=f;c.enter(a,null,y(u));u=f;J.clone=a;D[J.id]=J;e(J.scope,k,v,L,w,H,I)});q=D})}}}}],Ee=["$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",{tempClasses:"ng-hide-animate"})})}}}],xe=["$animate",
279 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",{tempClasses:"ng-hide-animate"})})}}}],Fe=Ma(function(a,c,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&m(d,function(a,d){c.css(d,"")});a&&c.css(a)},!0)}),Ge=["$animate",function(a){return{require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,d,e,f){var g=[],h=[],l=[],k=[],n=function(a,c){return function(){a.splice(c,1)}};c.$watch(e.ngSwitch||
280 e.on,function(c){var d,e;d=0;for(e=l.length;d<e;++d)a.cancel(l[d]);d=l.length=0;for(e=k.length;d<e;++d){var q=qb(h[d].clone);k[d].$destroy();(l[d]=a.leave(q)).then(n(l,d))}h.length=0;k.length=0;(g=f.cases["!"+c]||f.cases["?"])&&m(g,function(c){c.transclude(function(d,e){k.push(e);var f=c.element;d[d.length++]=U.createComment(" end ngSwitchWhen: ");h.push({clone:d});a.enter(d,f.parent(),f)})})})}}}],He=Ma({transclude:"element",priority:1200,require:"^ngSwitch",multiElement:!0,link:function(a,c,d,e,
281 f){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cases["!"+d.ngSwitchWhen].push({transclude:f,element:c})}}),Ie=Ma({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})}}),Ke=Ma({restrict:"EAC",link:function(a,c,d,e,f){if(!f)throw J("ngTransclude")("orphan",ua(c));f(function(a){c.empty();c.append(a)})}}),ke=["$templateCache",function(a){return{restrict:"E",terminal:!0,
282 compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],Ag={$setViewValue:v,$render:v},Bg=["$element","$scope","$attrs",function(a,c,d){var e=this,f=new Sa;e.ngModelCtrl=Ag;e.unknownOption=y(U.createElement("option"));e.renderUnknownOption=function(c){c="? "+Ga(c)+" ?";e.unknownOption.val(c);a.prepend(e.unknownOption);a.val(c)};c.$on("$destroy",function(){e.renderUnknownOption=v});e.removeUnknownOption=function(){e.unknownOption.parent()&&e.unknownOption.remove()};e.readValue=
283 function(){e.removeUnknownOption();return a.val()};e.writeValue=function(c){e.hasOption(c)?(e.removeUnknownOption(),a.val(c),""===c&&e.emptyOption.prop("selected",!0)):null==c&&e.emptyOption?(e.removeUnknownOption(),a.val("")):e.renderUnknownOption(c)};e.addOption=function(a,c){Ra(a,'"option value"');""===a&&(e.emptyOption=c);var d=f.get(a)||0;f.put(a,d+1)};e.removeOption=function(a){var c=f.get(a);c&&(1===c?(f.remove(a),""===a&&(e.emptyOption=t)):f.put(a,c-1))};e.hasOption=function(a){return!!f.get(a)}}],
284 le=function(){return{restrict:"E",require:["select","?ngModel"],controller:Bg,link:function(a,c,d,e){var f=e[1];if(f){var g=e[0];g.ngModelCtrl=f;f.$render=function(){g.writeValue(f.$viewValue)};c.on("change",function(){a.$apply(function(){f.$setViewValue(g.readValue())})});if(d.multiple){g.readValue=function(){var a=[];m(c.find("option"),function(c){c.selected&&a.push(c.value)});return a};g.writeValue=function(a){var d=new Sa(a);m(c.find("option"),function(a){a.selected=w(d.get(a.value))})};var h,
285 l=NaN;a.$watch(function(){l!==f.$viewValue||ka(h,f.$viewValue)||(h=ia(f.$viewValue),f.$render());l=f.$viewValue});f.$isEmpty=function(a){return!a||0===a.length}}}}}},ne=["$interpolate",function(a){function c(a){a[0].hasAttribute("selected")&&(a[0].selected=!0)}return{restrict:"E",priority:100,compile:function(d,e){if(A(e.value)){var f=a(d.text(),!0);f||e.$set("value",d.text())}return function(a,d,e){var k=d.parent(),m=k.data("$selectController")||k.parent().data("$selectController");m&&m.ngModelCtrl&&
286 (f?a.$watch(f,function(a,f){e.$set("value",a);f!==a&&m.removeOption(f);m.addOption(a,d);m.ngModelCtrl.$render();c(d)}):(m.addOption(e.value,d),m.ngModelCtrl.$render(),c(d)),d.on("$destroy",function(){m.removeOption(e.value);m.ngModelCtrl.$render()}))}}}}],me=ra({restrict:"E",terminal:!1}),Hc=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()}))}}},
287 Gc=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){L(a)&&0<a.length&&(a=new RegExp("^"+a+"$"));if(a&&!a.test)throw J("ngPattern")("noregexp",g,a,ua(c));f=a||t;e.$validate()});e.$validators.pattern=function(a){return e.$isEmpty(a)||A(f)||f.test(a)}}}}},Jc=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=-1;d.$observe("maxlength",function(a){a=W(a);f=isNaN(a)?-1:a;e.$validate()});
288 e.$validators.maxlength=function(a,c){return 0>f||e.$isEmpty(c)||c.length<=f}}}}},Ic=function(){return{restrict:"A",require:"?ngModel",link:function(a,c,d,e){if(e){var f=0;d.$observe("minlength",function(a){f=W(a)||0;e.$validate()});e.$validators.minlength=function(a,c){return e.$isEmpty(c)||c.length>=f}}}}};O.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):(ce(),ee(ca),y(U).ready(function(){Zd(U,Ac)}))})(window,document);!window.angular.$$csp()&&window.angular.element(document.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-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
214289 //# sourceMappingURL=angular.min.js.map
0 'use strict';
1
2 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
3 * Any commits to this file should be reviewed with security in mind. *
4 * Changes to this file can potentially create security vulnerabilities. *
5 * An approval from 2 Core members with history of modifying *
6 * this file is required. *
7 * *
8 * Does the change somehow allow for arbitrary javascript to be executed? *
9 * Or allows for someone to change the prototype of built-in objects? *
10 * Or gives undesired access to variables likes document or window? *
11 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
12
13 var $sanitizeMinErr = angular.$$minErr('$sanitize');
14
15 /**
16 * @ngdoc module
17 * @name ngSanitize
18 * @description
19 *
20 * # ngSanitize
21 *
22 * The `ngSanitize` module provides functionality to sanitize HTML.
23 *
24 *
25 * <div doc-module-components="ngSanitize"></div>
26 *
27 * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
28 */
29
30 /**
31 * @ngdoc service
32 * @name $sanitize
33 * @kind function
34 *
35 * @description
36 * Sanitizes an html string by stripping all potentially dangerous tokens.
37 *
38 * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
39 * then serialized back to properly escaped html string. This means that no unsafe input can make
40 * it into the returned string.
41 *
42 * The whitelist for URL sanitization of attribute values is configured using the functions
43 * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
44 * `$compileProvider`}.
45 *
46 * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
47 *
48 * @param {string} html HTML input.
49 * @returns {string} Sanitized HTML.
50 *
51 * @example
52 <example module="sanitizeExample" deps="angular-sanitize.js">
53 <file name="index.html">
54 <script>
55 angular.module('sanitizeExample', ['ngSanitize'])
56 .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
57 $scope.snippet =
58 '<p style="color:blue">an html\n' +
59 '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
60 'snippet</p>';
61 $scope.deliberatelyTrustDangerousSnippet = function() {
62 return $sce.trustAsHtml($scope.snippet);
63 };
64 }]);
65 </script>
66 <div ng-controller="ExampleController">
67 Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
68 <table>
69 <tr>
70 <td>Directive</td>
71 <td>How</td>
72 <td>Source</td>
73 <td>Rendered</td>
74 </tr>
75 <tr id="bind-html-with-sanitize">
76 <td>ng-bind-html</td>
77 <td>Automatically uses $sanitize</td>
78 <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
79 <td><div ng-bind-html="snippet"></div></td>
80 </tr>
81 <tr id="bind-html-with-trust">
82 <td>ng-bind-html</td>
83 <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
84 <td>
85 <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
86 &lt;/div&gt;</pre>
87 </td>
88 <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
89 </tr>
90 <tr id="bind-default">
91 <td>ng-bind</td>
92 <td>Automatically escapes</td>
93 <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
94 <td><div ng-bind="snippet"></div></td>
95 </tr>
96 </table>
97 </div>
98 </file>
99 <file name="protractor.js" type="protractor">
100 it('should sanitize the html snippet by default', function() {
101 expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
102 toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
103 });
104
105 it('should inline raw snippet if bound to a trusted value', function() {
106 expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
107 toBe("<p style=\"color:blue\">an html\n" +
108 "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
109 "snippet</p>");
110 });
111
112 it('should escape snippet without any filter', function() {
113 expect(element(by.css('#bind-default div')).getInnerHtml()).
114 toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
115 "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
116 "snippet&lt;/p&gt;");
117 });
118
119 it('should update', function() {
120 element(by.model('snippet')).clear();
121 element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
122 expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
123 toBe('new <b>text</b>');
124 expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
125 'new <b onclick="alert(1)">text</b>');
126 expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
127 "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
128 });
129 </file>
130 </example>
131 */
132
133
134 /**
135 * @ngdoc provider
136 * @name $sanitizeProvider
137 *
138 * @description
139 * Creates and configures {@link $sanitize} instance.
140 */
141 function $SanitizeProvider() {
142 var svgEnabled = false;
143
144 this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
145 if (svgEnabled) {
146 angular.extend(validElements, svgElements);
147 }
148 return function(html) {
149 var buf = [];
150 htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
151 return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
152 }));
153 return buf.join('');
154 };
155 }];
156
157
158 /**
159 * @ngdoc method
160 * @name $sanitizeProvider#enableSvg
161 * @kind function
162 *
163 * @description
164 * Enables a subset of svg to be supported by the sanitizer.
165 *
166 * <div class="alert alert-warning">
167 * <p>By enabling this setting without taking other precautions, you might expose your
168 * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
169 * outside of the containing element and be rendered over other elements on the page (e.g. a login
170 * link). Such behavior can then result in phishing incidents.</p>
171 *
172 * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
173 * tags within the sanitized content:</p>
174 *
175 * <br>
176 *
177 * <pre><code>
178 * .rootOfTheIncludedContent svg {
179 * overflow: hidden !important;
180 * }
181 * </code></pre>
182 * </div>
183 *
184 * @param {boolean=} regexp New regexp to whitelist urls with.
185 * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
186 * without an argument or self for chaining otherwise.
187 */
188 this.enableSvg = function(enableSvg) {
189 if (angular.isDefined(enableSvg)) {
190 svgEnabled = enableSvg;
191 return this;
192 } else {
193 return svgEnabled;
194 }
195 };
196 }
197
198 function sanitizeText(chars) {
199 var buf = [];
200 var writer = htmlSanitizeWriter(buf, angular.noop);
201 writer.chars(chars);
202 return buf.join('');
203 }
204
205
206 // Regular Expressions for parsing tags and attributes
207 var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
208 // Match everything outside of normal chars and " (quote character)
209 NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
210
211
212 // Good source of info about elements and attributes
213 // http://dev.w3.org/html5/spec/Overview.html#semantics
214 // http://simon.html5.org/html-elements
215
216 // Safe Void Elements - HTML5
217 // http://dev.w3.org/html5/spec/Overview.html#void-elements
218 var voidElements = toMap("area,br,col,hr,img,wbr");
219
220 // Elements that you can, intentionally, leave open (and which close themselves)
221 // http://dev.w3.org/html5/spec/Overview.html#optional-tags
222 var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
223 optionalEndTagInlineElements = toMap("rp,rt"),
224 optionalEndTagElements = angular.extend({},
225 optionalEndTagInlineElements,
226 optionalEndTagBlockElements);
227
228 // Safe Block Elements - HTML5
229 var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," +
230 "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
231 "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul"));
232
233 // Inline Elements - HTML5
234 var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," +
235 "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
236 "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
237
238 // SVG Elements
239 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
240 // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
241 // They can potentially allow for arbitrary javascript to be executed. See #11290
242 var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
243 "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
244 "radialGradient,rect,stop,svg,switch,text,title,tspan,use");
245
246 // Blocked Elements (will be stripped)
247 var blockedElements = toMap("script,style");
248
249 var validElements = angular.extend({},
250 voidElements,
251 blockElements,
252 inlineElements,
253 optionalEndTagElements);
254
255 //Attributes that have href and hence need to be sanitized
256 var uriAttrs = toMap("background,cite,href,longdesc,src,usemap,xlink:href");
257
258 var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
259 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
260 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
261 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
262 'valign,value,vspace,width');
263
264 // SVG attributes (without "id" and "name" attributes)
265 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
266 var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
267 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
268 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
269 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
270 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
271 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
272 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
273 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
274 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
275 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
276 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
277 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
278 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
279 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
280 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
281
282 var validAttrs = angular.extend({},
283 uriAttrs,
284 svgAttrs,
285 htmlAttrs);
286
287 function toMap(str, lowercaseKeys) {
288 var obj = {}, items = str.split(','), i;
289 for (i = 0; i < items.length; i++) {
290 obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
291 }
292 return obj;
293 }
294
295 var inertBodyElement;
296 (function(window) {
297 var doc;
298 if (window.document && window.document.implementation) {
299 doc = window.document.implementation.createHTMLDocument("inert");
300 } else {
301 throw $sanitizeMinErr('noinert', "Can't create an inert html document");
302 }
303 var docElement = doc.documentElement || doc.getDocumentElement();
304 var bodyElements = docElement.getElementsByTagName('body');
305
306 // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one
307 if (bodyElements.length === 1) {
308 inertBodyElement = bodyElements[0];
309 } else {
310 var html = doc.createElement('html');
311 inertBodyElement = doc.createElement('body');
312 html.appendChild(inertBodyElement);
313 doc.appendChild(html);
314 }
315 })(window);
316
317 /**
318 * @example
319 * htmlParser(htmlString, {
320 * start: function(tag, attrs) {},
321 * end: function(tag) {},
322 * chars: function(text) {},
323 * comment: function(text) {}
324 * });
325 *
326 * @param {string} html string
327 * @param {object} handler
328 */
329 function htmlParser(html, handler) {
330 if (html === null || html === undefined) {
331 html = '';
332 } else if (typeof html !== 'string') {
333 html = '' + html;
334 }
335 inertBodyElement.innerHTML = html;
336
337 //mXSS protection
338 var mXSSAttempts = 5;
339 do {
340 if (mXSSAttempts === 0) {
341 throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable");
342 }
343 mXSSAttempts--;
344
345 // strip custom-namespaced attributes on IE<=11
346 if (document.documentMode <= 11) {
347 stripCustomNsAttrs(inertBodyElement);
348 }
349 html = inertBodyElement.innerHTML; //trigger mXSS
350 inertBodyElement.innerHTML = html;
351 } while (html !== inertBodyElement.innerHTML);
352
353 var node = inertBodyElement.firstChild;
354 while (node) {
355 switch (node.nodeType) {
356 case 1: // ELEMENT_NODE
357 handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
358 break;
359 case 3: // TEXT NODE
360 handler.chars(node.textContent);
361 break;
362 }
363
364 var nextNode;
365 if (!(nextNode = node.firstChild)) {
366 if (node.nodeType == 1) {
367 handler.end(node.nodeName.toLowerCase());
368 }
369 nextNode = node.nextSibling;
370 if (!nextNode) {
371 while (nextNode == null) {
372 node = node.parentNode;
373 if (node === inertBodyElement) break;
374 nextNode = node.nextSibling;
375 if (node.nodeType == 1) {
376 handler.end(node.nodeName.toLowerCase());
377 }
378 }
379 }
380 }
381 node = nextNode;
382 }
383
384 while (node = inertBodyElement.firstChild) {
385 inertBodyElement.removeChild(node);
386 }
387 }
388
389 function attrToMap(attrs) {
390 var map = {};
391 for (var i = 0, ii = attrs.length; i < ii; i++) {
392 var attr = attrs[i];
393 map[attr.name] = attr.value;
394 }
395 return map;
396 }
397
398
399 /**
400 * Escapes all potentially dangerous characters, so that the
401 * resulting string can be safely inserted into attribute or
402 * element text.
403 * @param value
404 * @returns {string} escaped text
405 */
406 function encodeEntities(value) {
407 return value.
408 replace(/&/g, '&amp;').
409 replace(SURROGATE_PAIR_REGEXP, function(value) {
410 var hi = value.charCodeAt(0);
411 var low = value.charCodeAt(1);
412 return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
413 }).
414 replace(NON_ALPHANUMERIC_REGEXP, function(value) {
415 return '&#' + value.charCodeAt(0) + ';';
416 }).
417 replace(/</g, '&lt;').
418 replace(/>/g, '&gt;');
419 }
420
421 /**
422 * create an HTML/XML writer which writes to buffer
423 * @param {Array} buf use buf.join('') to get out sanitized html string
424 * @returns {object} in the form of {
425 * start: function(tag, attrs) {},
426 * end: function(tag) {},
427 * chars: function(text) {},
428 * comment: function(text) {}
429 * }
430 */
431 function htmlSanitizeWriter(buf, uriValidator) {
432 var ignoreCurrentElement = false;
433 var out = angular.bind(buf, buf.push);
434 return {
435 start: function(tag, attrs) {
436 tag = angular.lowercase(tag);
437 if (!ignoreCurrentElement && blockedElements[tag]) {
438 ignoreCurrentElement = tag;
439 }
440 if (!ignoreCurrentElement && validElements[tag] === true) {
441 out('<');
442 out(tag);
443 angular.forEach(attrs, function(value, key) {
444 var lkey=angular.lowercase(key);
445 var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
446 if (validAttrs[lkey] === true &&
447 (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
448 out(' ');
449 out(key);
450 out('="');
451 out(encodeEntities(value));
452 out('"');
453 }
454 });
455 out('>');
456 }
457 },
458 end: function(tag) {
459 tag = angular.lowercase(tag);
460 if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
461 out('</');
462 out(tag);
463 out('>');
464 }
465 if (tag == ignoreCurrentElement) {
466 ignoreCurrentElement = false;
467 }
468 },
469 chars: function(chars) {
470 if (!ignoreCurrentElement) {
471 out(encodeEntities(chars));
472 }
473 }
474 };
475 }
476
477
478 /**
479 * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
480 * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
481 * to allow any of these custom attributes. This method strips them all.
482 *
483 * @param node Root element to process
484 */
485 function stripCustomNsAttrs(node) {
486 if (node.nodeType === Node.ELEMENT_NODE) {
487 var attrs = node.attributes;
488 for (var i = 0, l = attrs.length; i < l; i++) {
489 var attrNode = attrs[i];
490 var attrName = attrNode.name.toLowerCase();
491 if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
492 node.removeAttributeNode(attrNode);
493 i--;
494 l--;
495 }
496 }
497 }
498
499 var nextNode = node.firstChild;
500 if (nextNode) {
501 stripCustomNsAttrs(nextNode);
502 }
503
504 nextNode = node.nextSibling;
505 if (nextNode) {
506 stripCustomNsAttrs(nextNode);
507 }
508 }
509
510
511
512 // define ngSanitize module and register $sanitize service
513 angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
+0
-10
views/reports/_attachments/script/ui-bootstrap-tpls-0.11.2.min.js less more
0 /*
1 * angular-ui-bootstrap
2 * http://angular-ui.github.io/bootstrap/
3
4 * Version: 0.11.2 - 2014-09-26
5 * License: MIT
6 */
7 angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",function(){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.$watch("isOpen",function(b){b&&d.closeOthers(a)}),a.toggleOpen=function(){a.isDisabled||(a.isOpen=!a.isOpen)}}}}).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,function(){}))}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){var d=b.hasClass(e.activeClass);(!d||angular.isDefined(c.uncheckable))&&a.$apply(function(){f.$setViewValue(d?null:a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=a.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=a.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.isActive=function(a){return i.currentSlide===a},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?i.select(b>=j.length?j[b-1]:j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",function(){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{active:"=?"},link:function(a,b,c,d){d.addSlide(a,b),a.$on("$destroy",function(){d.removeSlide(a)}),a.$watch("active",function(b){b&&d.select(a)})}}}),angular.module("ui.bootstrap.dateparser",[]).service("dateParser",["$locale","orderByFilter",function(a,b){function c(a){var c=[],d=a.split("");return angular.forEach(e,function(b,e){var f=a.indexOf(e);if(f>-1){a=a.split(""),d[f]="("+b.regex+")",a[f]="$";for(var g=f+1,h=f+e.length;h>g;g++)d[g]="",a[g]="$";a=a.join(""),c.push({index:f,apply:b.apply})}}),{regex:new RegExp("^"+d.join("")+"$"),map:b(c,"index")}}function d(a,b,c){return 1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}this.parsers={};var e={yyyy:{regex:"\\d{4}",apply:function(a){this.year=+a}},yy:{regex:"\\d{2}",apply:function(a){this.year=+a+2e3}},y:{regex:"\\d{1,4}",apply:function(a){this.year=+a}},MMMM:{regex:a.DATETIME_FORMATS.MONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.MONTH.indexOf(b)}},MMM:{regex:a.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(b){this.month=a.DATETIME_FORMATS.SHORTMONTH.indexOf(b)}},MM:{regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1}},M:{regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1}},dd:{regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},d:{regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},EEEE:{regex:a.DATETIME_FORMATS.DAY.join("|")},EEE:{regex:a.DATETIME_FORMATS.SHORTDAY.join("|")}};this.parse=function(b,e){if(!angular.isString(b)||!e)return b;e=a.DATETIME_FORMATS[e]||e,this.parsers[e]||(this.parsers[e]=c(e));var f=this.parsers[e],g=f.regex,h=f.map,i=b.match(g);if(i&&i.length){for(var j,k={year:1900,month:0,date:1,hours:0},l=1,m=i.length;m>l;l++){var n=h[l-1];n.apply&&n.apply.call(k,i[l])}return d(k.year,k.month,k.date)&&(j=new Date(k.year,k.month,k.date,k.hours)),j}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].documentElement.scrollLeft)}},positionElements:function(a,b,c,d){var e,f,g,h,i=c.split("-"),j=i[0],k=i[1]||"center";e=d?this.offset(a):this.position(a),f=b.prop("offsetWidth"),g=b.prop("offsetHeight");var l={center:function(){return e.left+e.width/2-f/2},left:function(){return e.left},right:function(){return e.left+e.width}},m={center:function(){return e.top+e.height/2-g/2},top:function(){return e.top},bottom:function(){return e.top+e.height}};switch(j){case"right":h={top:m[k](),left:l[j]()};break;case"left":h={top:m[k](),left:e.left-f};break;case"bottom":h={top:m[j](),left:l[k]()};break;default:h={top:e.top-g,left:l[k]()}}return h}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.position"]).constant("datepickerConfig",{formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",datepickerMode:"day",minMode:"day",maxMode:"year",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","$parse","$interpolate","$timeout","$log","dateFilter","datepickerConfig",function(a,b,c,d,e,f,g,h){var i=this,j={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","minMode","maxMode","showWeeks","startingDay","yearRange"],function(c,e){i[c]=angular.isDefined(b[c])?8>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):h[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){i[d]=a?new Date(a):null,i.refreshView()}):i[d]=h[d]?new Date(h[d]):null}),a.datepickerMode=a.datepickerMode||h.datepickerMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),this.activeDate=angular.isDefined(b.initDate)?a.$parent.$eval(b.initDate):new Date,a.isActive=function(b){return 0===i.compare(b.date,i.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){j=a,j.$render=function(){i.render()}},this.render=function(){if(j.$modelValue){var a=new Date(j.$modelValue),b=!isNaN(a);b?this.activeDate=a:f.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'),j.$setValidity("date",b)}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=j.$modelValue?new Date(j.$modelValue):null;j.$setValidity("date-disabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=j.$modelValue?new Date(j.$modelValue):null;return{date:a,label:g(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},a.select=function(b){if(a.datepickerMode===i.minMode){var c=j.$modelValue?new Date(j.$modelValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),j.$setViewValue(c),j.$render()}else i.activeDate=b,a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=i.activeDate.getFullYear()+a*(i.step.years||0),c=i.activeDate.getMonth()+a*(i.step.months||0);i.activeDate.setFullYear(b,c,1),i.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===i.maxMode&&1===b||a.datepickerMode===i.minMode&&-1===b||(a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var k=function(){e(function(){i.element[0].focus()},0,!1)};a.$on("datepicker.focus",k),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),b.stopPropagation(),"enter"===c||"space"===c){if(i.isDisabled(i.activeDate))return;a.select(i.activeDate),k()}else!b.ctrlKey||"up"!==c&&"down"!==c?(i.handleKeyDown(c,b),i.refreshView()):(a.toggleMode("up"===c?1:-1),k())}}]).directive("datepicker",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{datepickerMode:"=?",dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}).directive("daypicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/day.html",require:"^datepicker",link:function(b,c,d,e){function f(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?i[b]:29}function g(a,b){var c=new Array(b),d=new Date(a),e=0;for(d.setHours(12);b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}b.showWeeks=e.showWeeks,e.step={months:1},e.element=c;var i=[31,28,31,30,31,30,31,31,30,31,30,31];e._refreshView=function(){var c=e.activeDate.getFullYear(),d=e.activeDate.getMonth(),f=new Date(c,d,1),i=e.startingDay-f.getDay(),j=i>0?7-i:-i,k=new Date(f);j>0&&k.setDate(-j+1);for(var l=g(k,42),m=0;42>m;m++)l[m]=angular.extend(e.createDateObject(l[m],e.formatDay),{secondary:l[m].getMonth()!==d,uid:b.uniqueId+"-"+m});b.labels=new Array(7);for(var n=0;7>n;n++)b.labels[n]={abbr:a(l[n].date,e.formatDayHeader),full:a(l[n].date,"EEEE")};if(b.title=a(e.activeDate,e.formatDayTitle),b.rows=e.split(l,7),b.showWeeks){b.weekNumbers=[];for(var o=h(b.rows[0][0].date),p=b.rows.length;b.weekNumbers.push(o++)<p;);}},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},e.handleKeyDown=function(a){var b=e.activeDate.getDate();if("left"===a)b-=1;else if("up"===a)b-=7;else if("right"===a)b+=1;else if("down"===a)b+=7;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getMonth()+("pageup"===a?-1:1);e.activeDate.setMonth(c,1),b=Math.min(f(e.activeDate.getFullYear(),e.activeDate.getMonth()),b)}else"home"===a?b=1:"end"===a&&(b=f(e.activeDate.getFullYear(),e.activeDate.getMonth()));e.activeDate.setDate(b)},e.refreshView()}}}]).directive("monthpicker",["dateFilter",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/month.html",require:"^datepicker",link:function(b,c,d,e){e.step={years:1},e.element=c,e._refreshView=function(){for(var c=new Array(12),d=e.activeDate.getFullYear(),f=0;12>f;f++)c[f]=angular.extend(e.createDateObject(new Date(d,f,1),e.formatMonth),{uid:b.uniqueId+"-"+f});b.title=a(e.activeDate,e.formatMonthTitle),b.rows=e.split(c,3)},e.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},e.handleKeyDown=function(a){var b=e.activeDate.getMonth();if("left"===a)b-=1;else if("up"===a)b-=3;else if("right"===a)b+=1;else if("down"===a)b+=3;else if("pageup"===a||"pagedown"===a){var c=e.activeDate.getFullYear()+("pageup"===a?-1:1);e.activeDate.setFullYear(c)}else"home"===a?b=0:"end"===a&&(b=11);e.activeDate.setMonth(b)},e.refreshView()}}}]).directive("yearpicker",["dateFilter",function(){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/year.html",require:"^datepicker",link:function(a,b,c,d){function e(a){return parseInt((a-1)/f,10)*f+1}var f=d.yearRange;d.step={years:f},d.element=b,d._refreshView=function(){for(var b=new Array(f),c=0,g=e(d.activeDate.getFullYear());f>c;c++)b[c]=angular.extend(d.createDateObject(new Date(g+c,0,1),d.formatYear),{uid:a.uniqueId+"-"+c});a.title=[b[0].label,b[f-1].label].join(" - "),a.rows=d.split(b,5)},d.compare=function(a,b){return a.getFullYear()-b.getFullYear()},d.handleKeyDown=function(a){var b=d.activeDate.getFullYear();"left"===a?b-=1:"up"===a?b-=5:"right"===a?b+=1:"down"===a?b+=5:"pageup"===a||"pagedown"===a?b+=("pageup"===a?-1:1)*d.step.years:"home"===a?b=e(d.activeDate.getFullYear()):"end"===a&&(b=e(d.activeDate.getFullYear())+f-1),d.activeDate.setFullYear(b)},d.refreshView()}}}]).constant("datepickerPopupConfig",{datepickerPopup:"yyyy-MM-dd",currentText:"Today",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","dateParser","datepickerPopupConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&"},link:function(h,i,j,k){function l(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function m(a){if(a){if(angular.isDate(a)&&!isNaN(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=f.parse(a,n)||new Date(a);return isNaN(b)?void k.$setValidity("date",!1):(k.$setValidity("date",!0),b)}return void k.$setValidity("date",!1)}return k.$setValidity("date",!0),null}var n,o=angular.isDefined(j.closeOnDateSelection)?h.$parent.$eval(j.closeOnDateSelection):g.closeOnDateSelection,p=angular.isDefined(j.datepickerAppendToBody)?h.$parent.$eval(j.datepickerAppendToBody):g.appendToBody;h.showButtonBar=angular.isDefined(j.showButtonBar)?h.$parent.$eval(j.showButtonBar):g.showButtonBar,h.getText=function(a){return h[a+"Text"]||g[a+"Text"]},j.$observe("datepickerPopup",function(a){n=a||g.datepickerPopup,k.$render()});var q=angular.element("<div datepicker-popup-wrap><div datepicker></div></div>");q.attr({"ng-model":"date","ng-change":"dateSelection()"});var r=angular.element(q.children()[0]);j.datepickerOptions&&angular.forEach(h.$parent.$eval(j.datepickerOptions),function(a,b){r.attr(l(b),a)}),h.watchData={},angular.forEach(["minDate","maxDate","datepickerMode"],function(a){if(j[a]){var c=b(j[a]);if(h.$parent.$watch(c,function(b){h.watchData[a]=b}),r.attr(l(a),"watchData."+a),"datepickerMode"===a){var d=c.assign;h.$watch("watchData."+a,function(a,b){a!==b&&d(h.$parent,a)})}}}),j.dateDisabled&&r.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),k.$parsers.unshift(m),h.dateSelection=function(a){angular.isDefined(a)&&(h.date=a),k.$setViewValue(h.date),k.$render(),o&&(h.isOpen=!1,i[0].focus())},i.bind("input change keyup",function(){h.$apply(function(){h.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,n):"";i.val(a),h.date=m(k.$modelValue)};var s=function(a){h.isOpen&&a.target!==i[0]&&h.$apply(function(){h.isOpen=!1})},t=function(a){h.keydown(a)};i.bind("keydown",t),h.keydown=function(a){27===a.which?(a.preventDefault(),a.stopPropagation(),h.close()):40!==a.which||h.isOpen||(h.isOpen=!0)},h.$watch("isOpen",function(a){a?(h.$broadcast("datepicker.focus"),h.position=p?d.offset(i):d.position(i),h.position.top=h.position.top+i.prop("offsetHeight"),c.bind("click",s)):c.unbind("click",s)}),h.select=function(a){if("today"===a){var b=new Date;angular.isDate(k.$modelValue)?(a=new Date(k.$modelValue),a.setFullYear(b.getFullYear(),b.getMonth(),b.getDate())):a=new Date(b.setHours(0,0,0,0))}h.dateSelection(a)},h.close=function(){h.isOpen=!1,i[0].focus()};var u=a(q)(h);q.remove(),p?c.find("body").append(u):i.after(u),h.$on("$destroy",function(){u.remove(),i.unbind("keydown",t),c.unbind("click",s)})}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdown",[]).constant("dropdownConfig",{openClass:"open"}).service("dropdownService",["$document",function(a){var b=null;this.open=function(e){b||(a.bind("click",c),a.bind("keydown",d)),b&&b!==e&&(b.isOpen=!1),b=e},this.close=function(e){b===e&&(b=null,a.unbind("click",c),a.unbind("keydown",d))};var c=function(a){var c=b.getToggleElement();a&&c&&c[0].contains(a.target)||b.$apply(function(){b.isOpen=!1})},d=function(a){27===a.which&&(b.focusToggleElement(),c())}}]).controller("DropdownController",["$scope","$attrs","$parse","dropdownConfig","dropdownService","$animate",function(a,b,c,d,e,f){var g,h=this,i=a.$new(),j=d.openClass,k=angular.noop,l=b.onToggle?c(b.onToggle):angular.noop;this.init=function(d){h.$element=d,b.isOpen&&(g=c(b.isOpen),k=g.assign,a.$watch(g,function(a){i.isOpen=!!a}))},this.toggle=function(a){return i.isOpen=arguments.length?!!a:!i.isOpen},this.isOpen=function(){return i.isOpen},i.getToggleElement=function(){return h.toggleElement},i.focusToggleElement=function(){h.toggleElement&&h.toggleElement[0].focus()},i.$watch("isOpen",function(b,c){f[b?"addClass":"removeClass"](h.$element,j),b?(i.focusToggleElement(),e.open(i)):e.close(i),k(a,b),angular.isDefined(b)&&b!==c&&l(a,{open:!!b})}),a.$on("$locationChangeSuccess",function(){i.isOpen=!1}),a.$on("$destroy",function(){i.$destroy()})}]).directive("dropdown",function(){return{restrict:"CA",controller:"DropdownController",link:function(a,b,c,d){d.init(b)}}}).directive("dropdownToggle",function(){return{restrict:"CA",require:"?^dropdown",link:function(a,b,c,d){if(d){d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.transition"]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c<a.length;c++)if(b==a[c].key)return a[c]},keys:function(){for(var b=[],c=0;c<a.length;c++)b.push(a[c].key);return b},top:function(){return a[a.length-1]},remove:function(b){for(var c=-1,d=0;d<a.length;d++)if(b==a[d].key){c=d;break}return a.splice(c,1)[0]},removeTop:function(){return a.splice(a.length-1,1)[0]},length:function(){return a.length}}}}}).directive("modalBackdrop",["$timeout",function(a){return{restrict:"EA",replace:!0,templateUrl:"template/modal/backdrop.html",link:function(b,c,d){b.backdropClass=d.backdropClass||"",b.animate=!1,a(function(){b.animate=!0})}}}]).directive("modalWindow",["$modalStack","$timeout",function(a,b){return{restrict:"EA",scope:{index:"@",animate:"="},replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"template/modal/window.html"},link:function(c,d,e){d.addClass(e.windowClass||""),c.size=e.size,b(function(){c.animate=!0,d[0].querySelectorAll("[autofocus]").length||d[0].focus()}),c.close=function(b){var c=a.getTop();c&&c.value.backdrop&&"static"!=c.value.backdrop&&b.target===b.currentTarget&&(b.preventDefault(),b.stopPropagation(),a.dismiss(c.key,"backdrop click"))}}}}]).directive("modalTransclude",function(){return{link:function(a,b,c,d,e){e(a.$parent,function(a){b.empty(),b.append(a)})}}}).factory("$modalStack",["$transition","$timeout","$document","$compile","$rootScope","$$stackedMap",function(a,b,c,d,e,f){function g(){for(var a=-1,b=n.keys(),c=0;c<b.length;c++)n.get(b[c]).value.backdrop&&(a=c);return a}function h(a){var b=c.find("body").eq(0),d=n.get(a).value;n.remove(a),j(d.modalDomEl,d.modalScope,300,function(){d.modalScope.$destroy(),b.toggleClass(m,n.length()>0),i()})}function i(){if(k&&-1==g()){var a=l;j(k,l,150,function(){a.$destroy(),a=null}),k=void 0,l=void 0}}function j(c,d,e,f){function g(){g.done||(g.done=!0,c.remove(),f&&f())}d.animate=!1;var h=a.transitionEndEventName;if(h){var i=b(g,e);c.bind(h,function(){b.cancel(i),g(),d.$apply()})}else b(g)}var k,l,m="modal-open",n=f.createNew(),o={};return e.$watch(g,function(a){l&&(l.index=a)}),c.bind("keydown",function(a){var b;27===a.which&&(b=n.top(),b&&b.value.keyboard&&(a.preventDefault(),e.$apply(function(){o.dismiss(b.key,"escape key press")})))}),o.open=function(a,b){n.add(a,{deferred:b.deferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard});var f=c.find("body").eq(0),h=g();if(h>=0&&!k){l=e.$new(!0),l.index=h;var i=angular.element("<div modal-backdrop></div>");i.attr("backdrop-class",b.backdropClass),k=d(i)(l),f.append(k)}var j=angular.element("<div modal-window></div>");j.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,size:b.size,index:n.length()-1,animate:"animate"}).html(b.content);var o=d(j)(b.scope);n.top().value.modalDomEl=o,f.append(o),f.addClass(m)},o.close=function(a,b){var c=n.get(a);c&&(c.value.deferred.resolve(b),h(a))},o.dismiss=function(a,b){var c=n.get(a);c&&(c.value.deferred.reject(b),h(a))},o.dismissAll=function(a){for(var b=this.getTop();b;)this.dismiss(b.key,a),b=this.getTop()},o.getTop=function(){return n.top()},o}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(angular.isFunction(a.templateUrl)?a.templateUrl():a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i),b.controllerAs&&(d[b.controllerAs]=f)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,backdropClass:b.backdropClass,windowClass:b.windowClass,windowTemplateUrl:b.windowTemplateUrl,size:b.size})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse",function(a,b,c){var d=this,e={$setViewValue:angular.noop},f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(f,g){e=f,this.config=g,e.$render=function(){d.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){d.itemsPerPage=parseInt(b,10),a.totalPages=d.calculateTotalPages()}):this.itemsPerPage=g.itemsPerPage},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(e.$viewValue,10)||1},a.selectPage=function(b){a.page!==b&&b>0&&b<=a.totalPages&&(e.$setViewValue(b),e.$render())},a.getText=function(b){return a[b+"Text"]||d.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages},a.$watch("totalItems",function(){a.totalPages=d.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),a.page>b?a.selectPage(b):e.$render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@"},require:["pagination","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c){return{number:a,text:b,active:c}}function h(a,b){var c=[],d=1,e=b,f=angular.isDefined(k)&&b>k;f&&(l?(d=Math.max(a-Math.floor(k/2),1),e=d+k-1,e>b&&(e=b,d=e-k+1)):(d=(Math.ceil(a/k)-1)*k+1,e=Math.min(d+k-1,b)));for(var h=d;e>=h;h++){var i=g(h,h,h===a);c.push(i)}if(f&&!l){if(d>1){var j=g(d-1,"...",!1);c.unshift(j)}if(b>e){var m=g(e+1,"...",!1);c.push(m)}}return c}var i=f[0],j=f[1];if(j){var k=angular.isDefined(e.maxSize)?c.$parent.$eval(e.maxSize):b.maxSize,l=angular.isDefined(e.rotate)?c.$parent.$eval(e.rotate):b.rotate;c.boundaryLinks=angular.isDefined(e.boundaryLinks)?c.$parent.$eval(e.boundaryLinks):b.boundaryLinks,c.directionLinks=angular.isDefined(e.directionLinks)?c.$parent.$eval(e.directionLinks):b.directionLinks,i.init(j,b),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){k=parseInt(a,10),i.render()
8 });var m=i.render;i.render=function(){m(),c.page>0&&c.page<=c.totalPages&&(c.pages=h(c.page,c.totalPages))}}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@"},require:["pager","?ngModel"],controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&(b.align=angular.isDefined(d.align)?b.$parent.$eval(d.align):a.align,f.init(g,a))}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="<div "+p+'-popup title="'+q+"tt_title"+r+'" content="'+q+"tt_content"+r+'" placement="'+q+"tt_placement"+r+'" animation="tt_animation" is-open="tt_isOpen"></div>';return{restrict:"EA",scope:!0,compile:function(){var a=f(s);return function(b,c,d){function f(){b.tt_isOpen?m():k()}function k(){(!y||b.$eval(d[l+"Enable"]))&&(b.tt_popupDelay?v||(v=g(p,b.tt_popupDelay,!1),v.then(function(a){a()})):p()())}function m(){b.$apply(function(){q()})}function p(){return v=null,u&&(g.cancel(u),u=null),b.tt_content?(r(),t.css({top:0,left:0,display:"block"}),w?i.find("body").append(t):c.after(t),z(),b.tt_isOpen=!0,b.$digest(),z):angular.noop}function q(){b.tt_isOpen=!1,g.cancel(v),v=null,b.tt_animation?u||(u=g(s,500)):s()}function r(){t&&s(),t=a(b,function(){}),b.$digest()}function s(){u=null,t&&(t.remove(),t=null)}var t,u,v,w=angular.isDefined(o.appendToBody)?o.appendToBody:!1,x=n(void 0),y=angular.isDefined(d[l+"Enable"]),z=function(){var a=j.positionElements(c,t,b.tt_placement,w);a.top+="px",a.left+="px",t.css(a)};b.tt_isOpen=!1,d.$observe(e,function(a){b.tt_content=a,!a&&b.tt_isOpen&&q()}),d.$observe(l+"Title",function(a){b.tt_title=a}),d.$observe(l+"Placement",function(a){b.tt_placement=angular.isDefined(a)?a:o.placement}),d.$observe(l+"PopupDelay",function(a){var c=parseInt(a,10);b.tt_popupDelay=isNaN(c)?o.popupDelay:c});var A=function(){c.unbind(x.show,k),c.unbind(x.hide,m)};d.$observe(l+"Trigger",function(a){A(),x=n(a),x.show===x.hide?c.bind(x.show,f):(c.bind(x.show,k),c.bind(x.hide,m))});var B=b.$eval(d[l+"Animation"]);b.tt_animation=angular.isDefined(B)?!!B:o.animation,d.$observe(l+"AppendToBody",function(a){w=angular.isDefined(a)?h(a)(b):w}),w&&b.$on("$locationChangeSuccess",function(){b.tt_isOpen&&q()}),b.$on("$destroy",function(){g.cancel(u),g.cancel(v),A(),s()})}}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,this.addBar=function(b,c){e||c.css({transition:"none"}),this.bars.push(b),b.$watch("value",function(c){b.percent=+(100*c/a.max).toFixed(2)}),b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},templateUrl:"template/progressbar/progress.html"}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","ratingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(f)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff},a[b]);return a},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("rating",function(){return{restrict:"EA",require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect())}),a.active=!0,a.onSelect()},b.addTab=function(a){c.push(a),1===c.length?a.active=!0:a.active&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){b.$watch("active",function(a){a&&f.select(b)}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).controller("TimepickerController",["$scope","$attrs","$parse","$log","$locale","timepickerConfig",function(a,b,c,d,e,f){function g(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===p[1]&&(b+=12)),b):void 0}function h(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function i(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function j(a){k(),o.$setViewValue(new Date(n)),l(a)}function k(){o.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function l(b){var c=n.getHours(),d=n.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:i(c),a.minutes="m"===b?d:i(d),a.meridian=n.getHours()<12?p[0]:p[1]}function m(a){var b=new Date(n.getTime()+6e4*a);n.setHours(b.getHours(),b.getMinutes()),j()}var n=new Date,o={$setViewValue:angular.noop},p=angular.isDefined(b.meridians)?a.$parent.$eval(b.meridians):f.meridians||e.DATETIME_FORMATS.AMPMS;this.init=function(c,d){o=c,o.$render=this.render;var e=d.eq(0),g=d.eq(1),h=angular.isDefined(b.mousewheel)?a.$parent.$eval(b.mousewheel):f.mousewheel;h&&this.setupMousewheelEvents(e,g),a.readonlyInput=angular.isDefined(b.readonlyInput)?a.$parent.$eval(b.readonlyInput):f.readonlyInput,this.setupInputEvents(e,g)};var q=f.hourStep;b.hourStep&&a.$parent.$watch(c(b.hourStep),function(a){q=parseInt(a,10)});var r=f.minuteStep;b.minuteStep&&a.$parent.$watch(c(b.minuteStep),function(a){r=parseInt(a,10)}),a.showMeridian=f.showMeridian,b.showMeridian&&a.$parent.$watch(c(b.showMeridian),function(b){if(a.showMeridian=!!b,o.$error.time){var c=g(),d=h();angular.isDefined(c)&&angular.isDefined(d)&&(n.setHours(c),j())}else l()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){o.$setViewValue(null),o.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=g();angular.isDefined(a)?(n.setHours(a),j("h")):d(!0)},b.bind("blur",function(){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=i(a.hours)})}),a.updateMinutes=function(){var a=h();angular.isDefined(a)?(n.setMinutes(a),j("m")):d(void 0,!0)},c.bind("blur",function(){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=i(a.minutes)})})},this.render=function(){var a=o.$modelValue?new Date(o.$modelValue):null;isNaN(a)?(o.$setValidity("time",!1),d.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(n=a),k(),l())},a.incrementHours=function(){m(60*q)},a.decrementHours=function(){m(60*-q)},a.incrementMinutes=function(){m(r)},a.decrementMinutes=function(){m(-r)},a.toggleMeridian=function(){m(720*(n.getHours()<12?1:-1))}}]).directive("timepicker",function(){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?i.$eval(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=i.$new();i.$on("$destroy",function(){w.$destroy()});var x="typeahead-"+w.$id+"-"+Math.floor(1e4*Math.random());j.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":x});var y=angular.element("<div typeahead-popup></div>");y.attr({id:x,matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&y.attr("template-url",k.typeaheadTemplateUrl);var z=function(){w.matches=[],w.activeIdx=-1,j.attr("aria-expanded",!1)},A=function(a){return x+"-option-"+a};w.$watch("activeIdx",function(a){0>a?j.removeAttr("aria-activedescendant"):j.attr("aria-activedescendant",A(a))});var B=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){var d=a===l.$viewValue;if(d&&m)if(c.length>0){w.activeIdx=0,w.matches.length=0;for(var e=0;e<c.length;e++)b[v.itemName]=c[e],w.matches.push({id:A(e),label:v.viewMapper(w,b),model:c[e]});w.query=a,w.position=t?f.offset(j):f.position(j),w.position.top=w.position.top+j.prop("offsetHeight"),j.attr("aria-expanded",!0)}else z();d&&q(i,!1)},function(){z(),q(i,!1)})};z(),w.query=void 0;var C,D=function(a){C=d(function(){B(a)},o)},E=function(){C&&d.cancel(C)};l.$parsers.unshift(function(a){return m=!0,a&&a.length>=n?o>0?(E(),D(a)):B(a):(q(i,!1),E(),z()),p?a:a?void l.$setValidity("editable",!1):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),w.select=function(a){var b,c,e={};e[v.itemName]=c=w.matches[a].model,b=v.modelMapper(i,e),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,e)}),z(),d(function(){j[0].focus()},0,!1)},j.bind("keydown",function(a){0!==w.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(w.activeIdx=(w.activeIdx+1)%w.matches.length,w.$digest()):38===a.which?(w.activeIdx=(w.activeIdx?w.activeIdx:w.matches.length)-1,w.$digest()):13===a.which||9===a.which?w.$apply(function(){w.select(w.activeIdx)}):27===a.which&&(a.stopPropagation(),z(),w.$digest()))}),j.bind("blur",function(){m=!1});var F=function(a){j[0]!==a.target&&(z(),w.$digest())};e.bind("click",F),i.$on("$destroy",function(){e.unbind("click",F)});var G=a(y)(w);t?e.find("body").append(G):j.after(G)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?(""+b).replace(new RegExp(a(c),"gi"),"<strong>$&</strong>"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'<div class="panel panel-default">\n <div class="panel-heading">\n <h4 class="panel-title">\n <a class="accordion-toggle" ng-click="toggleOpen()" accordion-transclude="heading"><span ng-class="{\'text-muted\': isDisabled}">{{heading}}</span></a>\n </h4>\n </div>\n <div class="panel-collapse" collapse="!isOpen">\n <div class="panel-body" ng-transclude></div>\n </div>\n</div>')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'<div class="panel-group" ng-transclude></div>')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html",'<div class="alert" ng-class="[\'alert-\' + (type || \'warning\'), closeable ? \'alert-dismissable\' : null]" role="alert">\n <button ng-show="closeable" type="button" class="close" ng-click="close()">\n <span aria-hidden="true">&times;</span>\n <span class="sr-only">Close</span>\n </button>\n <div ng-transclude></div>\n</div>\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'<div ng-mouseenter="pause()" ng-mouseleave="play()" class="carousel" ng-swipe-right="prev()" ng-swipe-left="next()">\n <ol class="carousel-indicators" ng-show="slides.length > 1">\n <li ng-repeat="slide in slides track by $index" ng-class="{active: isActive(slide)}" ng-click="select(slide)"></li>\n </ol>\n <div class="carousel-inner" ng-transclude></div>\n <a class="left carousel-control" ng-click="prev()" ng-show="slides.length > 1"><span class="glyphicon glyphicon-chevron-left"></span></a>\n <a class="right carousel-control" ng-click="next()" ng-show="slides.length > 1"><span class="glyphicon glyphicon-chevron-right"></span></a>\n</div>\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html","<div ng-class=\"{\n 'active': leaving || (active && !entering),\n 'prev': (next || active) && direction=='prev',\n 'next': (next || active) && direction=='next',\n 'right': direction=='prev',\n 'left': direction=='next'\n }\" class=\"item text-center\" ng-transclude></div>\n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'<div ng-switch="datepickerMode" role="application" ng-keydown="keydown($event)">\n <daypicker ng-switch-when="day" tabindex="0"></daypicker>\n <monthpicker ng-switch-when="month" tabindex="0"></monthpicker>\n <yearpicker ng-switch-when="year" tabindex="0"></yearpicker>\n</div>')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/day.html",'<table role="grid" aria-labelledby="{{uniqueId}}-title" aria-activedescendant="{{activeDateId}}">\n <thead>\n <tr>\n <th><button type="button" class="btn btn-default btn-sm pull-left" ng-click="move(-1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-left"></i></button></th>\n <th colspan="{{5 + showWeeks}}"><button id="{{uniqueId}}-title" role="heading" aria-live="assertive" aria-atomic="true" type="button" class="btn btn-default btn-sm" ng-click="toggleMode()" tabindex="-1" style="width:100%;"><strong>{{title}}</strong></button></th>\n <th><button type="button" class="btn btn-default btn-sm pull-right" ng-click="move(1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-right"></i></button></th>\n </tr>\n <tr>\n <th ng-show="showWeeks" class="text-center"></th>\n <th ng-repeat="label in labels track by $index" class="text-center"><small aria-label="{{label.full}}">{{label.abbr}}</small></th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat="row in rows track by $index">\n <td ng-show="showWeeks" class="text-center h6"><em>{{ weekNumbers[$index] }}</em></td>\n <td ng-repeat="dt in row track by dt.date" class="text-center" role="gridcell" id="{{dt.uid}}" aria-disabled="{{!!dt.disabled}}">\n <button type="button" style="width:100%;" class="btn btn-default btn-sm" ng-class="{\'btn-info\': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="{\'text-muted\': dt.secondary, \'text-info\': dt.current}">{{dt.label}}</span></button>\n </td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/month.html",'<table role="grid" aria-labelledby="{{uniqueId}}-title" aria-activedescendant="{{activeDateId}}">\n <thead>\n <tr>\n <th><button type="button" class="btn btn-default btn-sm pull-left" ng-click="move(-1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-left"></i></button></th>\n <th><button id="{{uniqueId}}-title" role="heading" aria-live="assertive" aria-atomic="true" type="button" class="btn btn-default btn-sm" ng-click="toggleMode()" tabindex="-1" style="width:100%;"><strong>{{title}}</strong></button></th>\n <th><button type="button" class="btn btn-default btn-sm pull-right" ng-click="move(1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-right"></i></button></th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat="row in rows track by $index">\n <td ng-repeat="dt in row track by dt.date" class="text-center" role="gridcell" id="{{dt.uid}}" aria-disabled="{{!!dt.disabled}}">\n <button type="button" style="width:100%;" class="btn btn-default" ng-class="{\'btn-info\': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="{\'text-info\': dt.current}">{{dt.label}}</span></button>\n </td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html",'<ul class="dropdown-menu" ng-style="{display: (isOpen && \'block\') || \'none\', top: position.top+\'px\', left: position.left+\'px\'}" ng-keydown="keydown($event)">\n <li ng-transclude></li>\n <li ng-if="showButtonBar" style="padding:10px 9px 2px">\n <span class="btn-group">\n <button type="button" class="btn btn-sm btn-info" ng-click="select(\'today\')">{{ getText(\'current\') }}</button>\n <button type="button" class="btn btn-sm btn-danger" ng-click="select(null)">{{ getText(\'clear\') }}</button>\n </span>\n <button type="button" class="btn btn-sm btn-success pull-right" ng-click="close()">{{ getText(\'close\') }}</button>\n </li>\n</ul>\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/year.html",'<table role="grid" aria-labelledby="{{uniqueId}}-title" aria-activedescendant="{{activeDateId}}">\n <thead>\n <tr>\n <th><button type="button" class="btn btn-default btn-sm pull-left" ng-click="move(-1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-left"></i></button></th>\n <th colspan="3"><button id="{{uniqueId}}-title" role="heading" aria-live="assertive" aria-atomic="true" type="button" class="btn btn-default btn-sm" ng-click="toggleMode()" tabindex="-1" style="width:100%;"><strong>{{title}}</strong></button></th>\n <th><button type="button" class="btn btn-default btn-sm pull-right" ng-click="move(1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-right"></i></button></th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat="row in rows track by $index">\n <td ng-repeat="dt in row track by dt.date" class="text-center" role="gridcell" id="{{dt.uid}}" aria-disabled="{{!!dt.disabled}}">\n <button type="button" style="width:100%;" class="btn btn-default" ng-class="{\'btn-info\': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="{\'text-info\': dt.current}">{{dt.label}}</span></button>\n </td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'<div class="modal-backdrop fade {{ backdropClass }}"\n ng-class="{in: animate}"\n ng-style="{\'z-index\': 1040 + (index && 1 || 0) + index*10}"\n></div>\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'<div tabindex="-1" role="dialog" class="modal fade" ng-class="{in: animate}" ng-style="{\'z-index\': 1050 + index*10, display: \'block\'}" ng-click="close($event)">\n <div class="modal-dialog" ng-class="{\'modal-sm\': size == \'sm\', \'modal-lg\': size == \'lg\'}"><div class="modal-content" modal-transclude></div></div>\n</div>')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'<ul class="pager">\n <li ng-class="{disabled: noPrevious(), previous: align}"><a href ng-click="selectPage(page - 1)">{{getText(\'previous\')}}</a></li>\n <li ng-class="{disabled: noNext(), next: align}"><a href ng-click="selectPage(page + 1)">{{getText(\'next\')}}</a></li>\n</ul>')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'<ul class="pagination">\n <li ng-if="boundaryLinks" ng-class="{disabled: noPrevious()}"><a href ng-click="selectPage(1)">{{getText(\'first\')}}</a></li>\n <li ng-if="directionLinks" ng-class="{disabled: noPrevious()}"><a href ng-click="selectPage(page - 1)">{{getText(\'previous\')}}</a></li>\n <li ng-repeat="page in pages track by $index" ng-class="{active: page.active}"><a href ng-click="selectPage(page.number)">{{page.text}}</a></li>\n <li ng-if="directionLinks" ng-class="{disabled: noNext()}"><a href ng-click="selectPage(page + 1)">{{getText(\'next\')}}</a></li>\n <li ng-if="boundaryLinks" ng-class="{disabled: noNext()}"><a href ng-click="selectPage(totalPages)">{{getText(\'last\')}}</a></li>\n</ul>')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'<div class="tooltip {{placement}}" ng-class="{ in: isOpen(), fade: animation() }">\n <div class="tooltip-arrow"></div>\n <div class="tooltip-inner" bind-html-unsafe="content"></div>\n</div>\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'<div class="tooltip {{placement}}" ng-class="{ in: isOpen(), fade: animation() }">\n <div class="tooltip-arrow"></div>\n <div class="tooltip-inner" ng-bind="content"></div>\n</div>\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'<div class="popover {{placement}}" ng-class="{ in: isOpen(), fade: animation() }">\n <div class="arrow"></div>\n\n <div class="popover-inner">\n <h3 class="popover-title" ng-bind="title" ng-show="title"></h3>\n <div class="popover-content" ng-bind="content"></div>\n </div>\n</div>\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'<div class="progress-bar" ng-class="type && \'progress-bar-\' + type" role="progressbar" aria-valuenow="{{value}}" aria-valuemin="0" aria-valuemax="{{max}}" ng-style="{width: percent + \'%\'}" aria-valuetext="{{percent | number:0}}%" ng-transclude></div>')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'<div class="progress" ng-transclude></div>')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'<div class="progress">\n <div class="progress-bar" ng-class="type && \'progress-bar-\' + type" role="progressbar" aria-valuenow="{{value}}" aria-valuemin="0" aria-valuemax="{{max}}" ng-style="{width: percent + \'%\'}" aria-valuetext="{{percent | number:0}}%" ng-transclude></div>\n</div>')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'<span ng-mouseleave="reset()" ng-keydown="onKeydown($event)" tabindex="0" role="slider" aria-valuemin="0" aria-valuemax="{{range.length}}" aria-valuenow="{{value}}">\n <i ng-repeat="r in range track by $index" ng-mouseenter="enter($index + 1)" ng-click="rate($index + 1)" class="glyphicon" ng-class="$index < value && (r.stateOn || \'glyphicon-star\') || (r.stateOff || \'glyphicon-star-empty\')">\n <span class="sr-only">({{ $index < value ? \'*\' : \' \' }})</span>\n </i>\n</span>')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'<li ng-class="{active: active, disabled: disabled}">\n <a ng-click="select()" tab-heading-transclude>{{heading}}</a>\n</li>\n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'<div>\n <ul class="nav nav-{{type || \'tabs\'}}" ng-class="{\'nav-stacked\': vertical, \'nav-justified\': justified}" ng-transclude></ul>\n <div class="tab-content">\n <div class="tab-pane" \n ng-repeat="tab in tabs" \n ng-class="{active: tab.active}"\n tab-content-transclude="tab">\n </div>\n </div>\n</div>\n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'<table>\n <tbody>\n <tr class="text-center">\n <td><a ng-click="incrementHours()" class="btn btn-link"><span class="glyphicon glyphicon-chevron-up"></span></a></td>\n <td>&nbsp;</td>\n <td><a ng-click="incrementMinutes()" class="btn btn-link"><span class="glyphicon glyphicon-chevron-up"></span></a></td>\n <td ng-show="showMeridian"></td>\n </tr>\n <tr>\n <td style="width:50px;" class="form-group" ng-class="{\'has-error\': invalidHours}">\n <input type="text" ng-model="hours" ng-change="updateHours()" class="form-control text-center" ng-mousewheel="incrementHours()" ng-readonly="readonlyInput" maxlength="2">\n </td>\n <td>:</td>\n <td style="width:50px;" class="form-group" ng-class="{\'has-error\': invalidMinutes}">\n <input type="text" ng-model="minutes" ng-change="updateMinutes()" class="form-control text-center" ng-readonly="readonlyInput" maxlength="2">\n </td>\n <td ng-show="showMeridian"><button type="button" class="btn btn-default text-center" ng-click="toggleMeridian()">{{meridian}}</button></td>\n </tr>\n <tr class="text-center">\n <td><a ng-click="decrementHours()" class="btn btn-link"><span class="glyphicon glyphicon-chevron-down"></span></a></td>\n <td>&nbsp;</td>\n <td><a ng-click="decrementMinutes()" class="btn btn-link"><span class="glyphicon glyphicon-chevron-down"></span></a></td>\n <td ng-show="showMeridian"></td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'<a tabindex="-1" bind-html-unsafe="match.label | typeaheadHighlight:query"></a>')
9 }]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html",'<ul class="dropdown-menu" ng-show="isOpen()" ng-style="{top: position.top+\'px\', left: position.left+\'px\'}" style="display: block;" role="listbox" aria-hidden="{{!isOpen()}}">\n <li ng-repeat="match in matches track by $index" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)" ng-click="selectMatch($index)" role="option" id="{{match.id}}">\n <div typeahead-match index="$index" match="match" query="query" template-url="templateUrl"></div>\n </li>\n</ul>\n')}]);
0 /*
1 * angular-ui-bootstrap
2 * http://angular-ui.github.io/bootstrap/
3
4 * Version: 0.14.1 - 2015-10-11
5 * License: MIT
6 */
7 angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-popup.html","template/tooltip/tooltip-popup.html","template/tooltip/tooltip-template-popup.html","template/popover/popover-html.html","template/popover/popover-template.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.collapse",[]).directive("uibCollapse",["$animate","$injector",function(a,b){var c=b.has("$animateCss")?b.get("$animateCss"):null;return{link:function(b,d,e){function f(){d.removeClass("collapse").addClass("collapsing").attr("aria-expanded",!0).attr("aria-hidden",!1),c?c(d,{addClass:"in",easing:"ease",to:{height:d[0].scrollHeight+"px"}}).start().done(g):a.addClass(d,"in",{to:{height:d[0].scrollHeight+"px"}}).then(g)}function g(){d.removeClass("collapsing").addClass("collapse").css({height:"auto"})}function h(){return d.hasClass("collapse")||d.hasClass("in")?(d.css({height:d[0].scrollHeight+"px"}).removeClass("collapse").addClass("collapsing").attr("aria-expanded",!1).attr("aria-hidden",!0),void(c?c(d,{removeClass:"in",to:{height:"0"}}).start().done(i):a.removeClass(d,"in",{to:{height:"0"}}).then(i))):i()}function i(){d.css({height:"0"}),d.removeClass("collapsing").addClass("collapse")}b.$watch(e.uibCollapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.collapse").value("$collapseSuppressWarning",!1).directive("collapse",["$animate","$injector","$log","$collapseSuppressWarning",function(a,b,c,d){var e=b.has("$animateCss")?b.get("$animateCss"):null;return{link:function(b,f,g){function h(){f.removeClass("collapse").addClass("collapsing").attr("aria-expanded",!0).attr("aria-hidden",!1),e?e(f,{addClass:"in",easing:"ease",to:{height:f[0].scrollHeight+"px"}}).start().done(i):a.addClass(f,"in",{to:{height:f[0].scrollHeight+"px"}}).then(i)}function i(){f.removeClass("collapsing").addClass("collapse").css({height:"auto"})}function j(){return f.hasClass("collapse")||f.hasClass("in")?(f.css({height:f[0].scrollHeight+"px"}).removeClass("collapse").addClass("collapsing").attr("aria-expanded",!1).attr("aria-hidden",!0),void(e?e(f,{removeClass:"in",to:{height:"0"}}).start().done(k):a.removeClass(f,"in",{to:{height:"0"}}).then(k))):k()}function k(){f.css({height:"0"}),f.removeClass("collapsing").addClass("collapse")}d||c.warn("collapse is now deprecated. Use uib-collapse instead."),b.$watch(g.collapse,function(a){a?j():h()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("uibAccordionConfig",{closeOthers:!0}).controller("UibAccordionController",["$scope","$attrs","uibAccordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(c){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("uibAccordion",function(){return{controller:"UibAccordionController",controllerAs:"accordion",transclude:!0,templateUrl:function(a,b){return b.templateUrl||"template/accordion/accordion.html"}}}).directive("uibAccordionGroup",function(){return{require:"^uibAccordion",transclude:!0,replace:!0,templateUrl:function(a,b){return b.templateUrl||"template/accordion/accordion-group.html"},scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.openClass=c.openClass||"panel-open",a.panelClass=c.panelClass,a.$watch("isOpen",function(c){b.toggleClass(a.openClass,!!c),c&&d.closeOthers(a)}),a.toggleOpen=function(b){a.isDisabled||b&&32!==b.which||(a.isOpen=!a.isOpen)}}}}).directive("uibAccordionHeading",function(){return{transclude:!0,template:"",replace:!0,require:"^uibAccordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,angular.noop))}}}).directive("uibAccordionTransclude",function(){return{require:["?^uibAccordionGroup","?^accordionGroup"],link:function(a,b,c,d){d=d[0]?d[0]:d[1],a.$watch(function(){return d[c.uibAccordionTransclude]},function(a){a&&(b.find("span").html(""),b.find("span").append(a))})}}}),angular.module("ui.bootstrap.accordion").value("$accordionSuppressWarning",!1).controller("AccordionController",["$scope","$attrs","$controller","$log","$accordionSuppressWarning",function(a,b,c,d,e){e||d.warn("AccordionController is now deprecated. Use UibAccordionController instead."),angular.extend(this,c("UibAccordionController",{$scope:a,$attrs:b}))}]).directive("accordion",["$log","$accordionSuppressWarning",function(a,b){return{restrict:"EA",controller:"AccordionController",controllerAs:"accordion",transclude:!0,replace:!1,templateUrl:function(a,b){return b.templateUrl||"template/accordion/accordion.html"},link:function(){b||a.warn("accordion is now deprecated. Use uib-accordion instead.")}}}]).directive("accordionGroup",["$log","$accordionSuppressWarning",function(a,b){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:function(a,b){return b.templateUrl||"template/accordion/accordion-group.html"},scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(c,d,e,f){b||a.warn("accordion-group is now deprecated. Use uib-accordion-group instead."),f.addGroup(c),c.openClass=e.openClass||"panel-open",c.panelClass=e.panelClass,c.$watch("isOpen",function(a){d.toggleClass(c.openClass,!!a),a&&f.closeOthers(c)}),c.toggleOpen=function(a){c.isDisabled||a&&32!==a.which||(c.isOpen=!c.isOpen)}}}}]).directive("accordionHeading",["$log","$accordionSuppressWarning",function(a,b){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",link:function(c,d,e,f,g){b||a.warn("accordion-heading is now deprecated. Use uib-accordion-heading instead."),f.setHeading(g(c,angular.noop))}}}]).directive("accordionTransclude",["$log","$accordionSuppressWarning",function(a,b){return{require:"^accordionGroup",link:function(c,d,e,f){b||a.warn("accordion-transclude is now deprecated. Use uib-accordion-transclude instead."),c.$watch(function(){return f[e.accordionTransclude]},function(a){a&&(d.find("span").html(""),d.find("span").append(a))})}}}]),angular.module("ui.bootstrap.alert",[]).controller("UibAlertController",["$scope","$attrs","$timeout",function(a,b,c){a.closeable=!!b.close,angular.isDefined(b.dismissOnTimeout)&&c(function(){a.close()},parseInt(b.dismissOnTimeout,10))}]).directive("uibAlert",function(){return{controller:"UibAlertController",controllerAs:"alert",templateUrl:function(a,b){return b.templateUrl||"template/alert/alert.html"},transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.alert").value("$alertSuppressWarning",!1).controller("AlertController",["$scope","$attrs","$controller","$log","$alertSuppressWarning",function(a,b,c,d,e){e||d.warn("AlertController is now deprecated. Use UibAlertController instead."),angular.extend(this,c("UibAlertController",{$scope:a,$attrs:b}))}]).directive("alert",["$log","$alertSuppressWarning",function(a,b){return{controller:"AlertController",controllerAs:"alert",templateUrl:function(a,b){return b.templateUrl||"template/alert/alert.html"},transclude:!0,replace:!0,scope:{type:"@",close:"&"},link:function(){b||a.warn("alert is now deprecated. Use uib-alert instead.")}}}]),angular.module("ui.bootstrap.buttons",[]).constant("uibButtonConfig",{activeClass:"active",toggleEvent:"click"}).controller("UibButtonsController",["uibButtonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("uibBtnRadio",function(){return{require:["uibBtnRadio","ngModel"],controller:"UibButtonsController",controllerAs:"buttons",link:function(a,b,c,d){var e=d[0],f=d[1];b.find("input").css({display:"none"}),f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.uibBtnRadio)))},b.on(e.toggleEvent,function(){if(!c.disabled){var d=b.hasClass(e.activeClass);(!d||angular.isDefined(c.uncheckable))&&a.$apply(function(){f.$setViewValue(d?null:a.$eval(c.uibBtnRadio)),f.$render()})}})}}}).directive("uibBtnCheckbox",["$document",function(a){return{require:["uibBtnCheckbox","ngModel"],controller:"UibButtonsController",controllerAs:"button",link:function(b,c,d,e){function f(){return h(d.btnCheckboxTrue,!0)}function g(){return h(d.btnCheckboxFalse,!1)}function h(a,c){return angular.isDefined(a)?b.$eval(a):c}var i=e[0],j=e[1];c.find("input").css({display:"none"}),j.$render=function(){c.toggleClass(i.activeClass,angular.equals(j.$modelValue,f()))},c.on(i.toggleEvent,function(){d.disabled||b.$apply(function(){j.$setViewValue(c.hasClass(i.activeClass)?g():f()),j.$render()})}),c.on("keypress",function(e){d.disabled||32!==e.which||a[0].activeElement!==c[0]||b.$apply(function(){j.$setViewValue(c.hasClass(i.activeClass)?g():f()),j.$render()})})}}}]),angular.module("ui.bootstrap.buttons").value("$buttonsSuppressWarning",!1).controller("ButtonsController",["$controller","$log","$buttonsSuppressWarning",function(a,b,c){c||b.warn("ButtonsController is now deprecated. Use UibButtonsController instead."),angular.extend(this,a("UibButtonsController"))}]).directive("btnRadio",["$log","$buttonsSuppressWarning",function(a,b){return{require:["btnRadio","ngModel"],controller:"ButtonsController",controllerAs:"buttons",link:function(c,d,e,f){b||a.warn("btn-radio is now deprecated. Use uib-btn-radio instead.");var g=f[0],h=f[1];d.find("input").css({display:"none"}),h.$render=function(){d.toggleClass(g.activeClass,angular.equals(h.$modelValue,c.$eval(e.btnRadio)))},d.bind(g.toggleEvent,function(){if(!e.disabled){var a=d.hasClass(g.activeClass);(!a||angular.isDefined(e.uncheckable))&&c.$apply(function(){h.$setViewValue(a?null:c.$eval(e.btnRadio)),h.$render()})}})}}}]).directive("btnCheckbox",["$document","$log","$buttonsSuppressWarning",function(a,b,c){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",controllerAs:"button",link:function(d,e,f,g){function h(){return j(f.btnCheckboxTrue,!0)}function i(){return j(f.btnCheckboxFalse,!1)}function j(a,b){var c=d.$eval(a);return angular.isDefined(c)?c:b}c||b.warn("btn-checkbox is now deprecated. Use uib-btn-checkbox instead.");var k=g[0],l=g[1];e.find("input").css({display:"none"}),l.$render=function(){e.toggleClass(k.activeClass,angular.equals(l.$modelValue,h()))},e.bind(k.toggleEvent,function(){f.disabled||d.$apply(function(){l.$setViewValue(e.hasClass(k.activeClass)?i():h()),l.$render()})}),e.on("keypress",function(b){f.disabled||32!==b.which||a[0].activeElement!==e[0]||d.$apply(function(){l.$setViewValue(e.hasClass(k.activeClass)?i():h()),l.$render()})})}}}]),angular.module("ui.bootstrap.carousel",[]).controller("UibCarouselController",["$scope","$element","$interval","$animate",function(a,b,c,d){function e(b,c,e){s||(angular.extend(b,{direction:e,active:!0}),angular.extend(m.currentSlide||{},{direction:e,active:!1}),d.enabled()&&!a.noTransition&&!a.$currentTransition&&b.$element&&m.slides.length>1&&(b.$element.data(q,b.direction),m.currentSlide&&m.currentSlide.$element&&m.currentSlide.$element.data(q,b.direction),a.$currentTransition=!0,o?d.on("addClass",b.$element,function(b,c){"close"===c&&(a.$currentTransition=null,d.off("addClass",b))}):b.$element.one("$animate:close",function(){a.$currentTransition=null})),m.currentSlide=b,r=c,g())}function f(a){if(angular.isUndefined(n[a].index))return n[a];var b;n.length;for(b=0;b<n.length;++b)if(n[b].index==a)return n[b]}function g(){h();var b=+a.interval;!isNaN(b)&&b>0&&(k=c(i,b))}function h(){k&&(c.cancel(k),k=null)}function i(){var b=+a.interval;l&&!isNaN(b)&&b>0&&n.length?a.next():a.pause()}function j(b){b.length||(a.$currentTransition=null)}var k,l,m=this,n=m.slides=a.slides=[],o=angular.version.minor>=4,p="uib-noTransition",q="uib-slideDirection",r=-1;m.currentSlide=null;var s=!1;m.select=a.select=function(b,c){var d=a.indexOfSlide(b);void 0===c&&(c=d>m.getCurrentIndex()?"next":"prev"),b&&b!==m.currentSlide&&!a.$currentTransition&&e(b,d,c)},a.$on("$destroy",function(){s=!0}),m.getCurrentIndex=function(){return m.currentSlide&&angular.isDefined(m.currentSlide.index)?+m.currentSlide.index:r},a.indexOfSlide=function(a){return angular.isDefined(a.index)?+a.index:n.indexOf(a)},a.next=function(){var b=(m.getCurrentIndex()+1)%n.length;return 0===b&&a.noWrap()?void a.pause():m.select(f(b),"next")},a.prev=function(){var b=m.getCurrentIndex()-1<0?n.length-1:m.getCurrentIndex()-1;return a.noWrap()&&b===n.length-1?void a.pause():m.select(f(b),"prev")},a.isActive=function(a){return m.currentSlide===a},a.$watch("interval",g),a.$watchCollection("slides",j),a.$on("$destroy",h),a.play=function(){l||(l=!0,g())},a.pause=function(){a.noPause||(l=!1,h())},m.addSlide=function(b,c){b.$element=c,n.push(b),1===n.length||b.active?(m.select(n[n.length-1]),1===n.length&&a.play()):b.active=!1},m.removeSlide=function(a){angular.isDefined(a.index)&&n.sort(function(a,b){return+a.index>+b.index});var b=n.indexOf(a);n.splice(b,1),n.length>0&&a.active?b>=n.length?m.select(n[b-1]):m.select(n[b]):r>b&&r--,0===n.length&&(m.currentSlide=null)},a.$watch("noTransition",function(a){b.data(p,a)})}]).directive("uibCarousel",[function(){return{transclude:!0,replace:!0,controller:"UibCarouselController",controllerAs:"carousel",require:"carousel",templateUrl:function(a,b){return b.templateUrl||"template/carousel/carousel.html"},scope:{interval:"=",noTransition:"=",noPause:"=",noWrap:"&"}}}]).directive("uibSlide",function(){return{require:"^uibCarousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:function(a,b){return b.templateUrl||"template/carousel/slide.html"},scope:{active:"=?",actual:"=?",index:"=?"},link:function(a,b,c,d){d.addSlide(a,b),a.$on("$destroy",function(){d.removeSlide(a)}),a.$watch("active",function(b){b&&d.select(a)})}}}).animation(".item",["$injector","$animate",function(a,b){function c(a,b,c){a.removeClass(b),c&&c()}var d="uib-noTransition",e="uib-slideDirection",f=null;return a.has("$animateCss")&&(f=a.get("$animateCss")),{beforeAddClass:function(a,g,h){if("active"==g&&a.parent()&&a.parent().parent()&&!a.parent().parent().data(d)){var i=!1,j=a.data(e),k="next"==j?"left":"right",l=c.bind(this,a,k+" "+j,h);return a.addClass(j),f?f(a,{addClass:k}).start().done(l):b.addClass(a,k).then(function(){i||l(),h()}),function(){i=!0}}h()},beforeRemoveClass:function(a,g,h){if("active"===g&&a.parent()&&a.parent().parent()&&!a.parent().parent().data(d)){var i=!1,j=a.data(e),k="next"==j?"left":"right",l=c.bind(this,a,k,h);return f?f(a,{addClass:k}).start().done(l):b.addClass(a,k).then(function(){i||l(),h()}),function(){i=!0}}h()}}}]),angular.module("ui.bootstrap.carousel").value("$carouselSuppressWarning",!1).controller("CarouselController",["$scope","$element","$controller","$log","$carouselSuppressWarning",function(a,b,c,d,e){e||d.warn("CarouselController is now deprecated. Use UibCarouselController instead."),angular.extend(this,c("UibCarouselController",{$scope:a,$element:b}))}]).directive("carousel",["$log","$carouselSuppressWarning",function(a,b){return{transclude:!0,replace:!0,controller:"CarouselController",controllerAs:"carousel",require:"carousel",templateUrl:function(a,b){return b.templateUrl||"template/carousel/carousel.html"},scope:{interval:"=",noTransition:"=",noPause:"=",noWrap:"&"},link:function(){b||a.warn("carousel is now deprecated. Use uib-carousel instead.")}}}]).directive("slide",["$log","$carouselSuppressWarning",function(a,b){return{require:"^carousel",transclude:!0,replace:!0,templateUrl:function(a,b){return b.templateUrl||"template/carousel/slide.html"},scope:{active:"=?",actual:"=?",index:"=?"},link:function(c,d,e,f){b||a.warn("slide is now deprecated. Use uib-slide instead."),f.addSlide(c,d),c.$on("$destroy",function(){f.removeSlide(c)}),c.$watch("active",function(a){a&&f.select(c)})}}}]),angular.module("ui.bootstrap.dateparser",[]).service("uibDateParser",["$log","$locale","orderByFilter",function(a,b,c){function d(a){var b=[],d=a.split("");return angular.forEach(g,function(c,e){var f=a.indexOf(e);if(f>-1){a=a.split(""),d[f]="("+c.regex+")",a[f]="$";for(var g=f+1,h=f+e.length;h>g;g++)d[g]="",a[g]="$";a=a.join(""),b.push({index:f,apply:c.apply})}}),{regex:new RegExp("^"+d.join("")+"$"),map:c(b,"index")}}function e(a,b,c){return 1>c?!1:1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}var f,g,h=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;this.init=function(){f=b.id,this.parsers={},g={yyyy:{regex:"\\d{4}",apply:function(a){this.year=+a}},yy:{regex:"\\d{2}",apply:function(a){this.year=+a+2e3}},y:{regex:"\\d{1,4}",apply:function(a){this.year=+a}},MMMM:{regex:b.DATETIME_FORMATS.MONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.MONTH.indexOf(a)}},MMM:{regex:b.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.SHORTMONTH.indexOf(a)}},MM:{regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1}},M:{regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1}},dd:{regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},d:{regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a}},EEEE:{regex:b.DATETIME_FORMATS.DAY.join("|")},EEE:{regex:b.DATETIME_FORMATS.SHORTDAY.join("|")},HH:{regex:"(?:0|1)[0-9]|2[0-3]",apply:function(a){this.hours=+a}},hh:{regex:"0[0-9]|1[0-2]",apply:function(a){this.hours=+a}},H:{regex:"1?[0-9]|2[0-3]",apply:function(a){this.hours=+a}},h:{regex:"[0-9]|1[0-2]",apply:function(a){this.hours=+a}},mm:{regex:"[0-5][0-9]",apply:function(a){this.minutes=+a}},m:{regex:"[0-9]|[1-5][0-9]",apply:function(a){this.minutes=+a}},sss:{regex:"[0-9][0-9][0-9]",apply:function(a){this.milliseconds=+a}},ss:{regex:"[0-5][0-9]",apply:function(a){this.seconds=+a}},s:{regex:"[0-9]|[1-5][0-9]",apply:function(a){this.seconds=+a}},a:{regex:b.DATETIME_FORMATS.AMPMS.join("|"),apply:function(a){12===this.hours&&(this.hours=0),"PM"===a&&(this.hours+=12)}}}},this.init(),this.parse=function(c,g,i){if(!angular.isString(c)||!g)return c;g=b.DATETIME_FORMATS[g]||g,g=g.replace(h,"\\$&"),b.id!==f&&this.init(),this.parsers[g]||(this.parsers[g]=d(g));var j=this.parsers[g],k=j.regex,l=j.map,m=c.match(k);if(m&&m.length){var n,o;angular.isDate(i)&&!isNaN(i.getTime())?n={year:i.getFullYear(),month:i.getMonth(),date:i.getDate(),hours:i.getHours(),minutes:i.getMinutes(),seconds:i.getSeconds(),milliseconds:i.getMilliseconds()}:(i&&a.warn("dateparser:","baseDate is not a valid date"),n={year:1900,month:0,date:1,hours:0,minutes:0,seconds:0,milliseconds:0});for(var p=1,q=m.length;q>p;p++){var r=l[p-1];r.apply&&r.apply.call(n,m[p])}return e(n.year,n.month,n.date)&&(o=new Date(n.year,n.month,n.date,n.hours,n.minutes,n.seconds,n.milliseconds||0)),o}}}]),angular.module("ui.bootstrap.dateparser").value("$dateParserSuppressWarning",!1).service("dateParser",["$log","$dateParserSuppressWarning","uibDateParser",function(a,b,c){b||a.warn("dateParser is now deprecated. Use uibDateParser instead."),angular.extend(this,c)}]),angular.module("ui.bootstrap.position",[]).factory("$uibPosition",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].documentElement.scrollLeft)}},positionElements:function(a,b,c,d){var e,f,g,h,i=c.split("-"),j=i[0],k=i[1]||"center";e=d?this.offset(a):this.position(a),f=b.prop("offsetWidth"),g=b.prop("offsetHeight");var l={center:function(){return e.left+e.width/2-f/2},left:function(){return e.left},right:function(){return e.left+e.width}},m={center:function(){return e.top+e.height/2-g/2},top:function(){return e.top},bottom:function(){return e.top+e.height}};switch(j){case"right":h={top:m[k](),left:l[j]()};break;case"left":h={top:m[k](),left:e.left-f};break;case"bottom":h={top:m[j](),left:l[k]()};break;default:h={top:e.top-g,left:l[k]()}}return h}}}]),angular.module("ui.bootstrap.position").value("$positionSuppressWarning",!1).service("$position",["$log","$positionSuppressWarning","$uibPosition",function(a,b,c){b||a.warn("$position is now deprecated. Use $uibPosition instead."),angular.extend(this,c)}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.position"]).value("$datepickerSuppressError",!1).constant("uibDatepickerConfig",{formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",datepickerMode:"day",minMode:"day",maxMode:"year",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null,shortcutPropagation:!1}).controller("UibDatepickerController",["$scope","$attrs","$parse","$interpolate","$log","dateFilter","uibDatepickerConfig","$datepickerSuppressError",function(a,b,c,d,e,f,g,h){var i=this,j={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","showWeeks","startingDay","yearRange","shortcutPropagation"],function(c,e){i[c]=angular.isDefined(b[c])?6>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):g[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){i[d]=a?new Date(a):null,i.refreshView()}):i[d]=g[d]?new Date(g[d]):null}),angular.forEach(["minMode","maxMode"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(c){i[d]=angular.isDefined(c)?c:b[d],a[d]=i[d],("minMode"==d&&i.modes.indexOf(a.datepickerMode)<i.modes.indexOf(i[d])||"maxMode"==d&&i.modes.indexOf(a.datepickerMode)>i.modes.indexOf(i[d]))&&(a.datepickerMode=i[d])}):(i[d]=g[d]||null,a[d]=i[d])}),a.datepickerMode=a.datepickerMode||g.datepickerMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),angular.isDefined(b.initDate)?(this.activeDate=a.$parent.$eval(b.initDate)||new Date,a.$parent.$watch(b.initDate,function(a){a&&(j.$isEmpty(j.$modelValue)||j.$invalid)&&(i.activeDate=a,i.refreshView())})):this.activeDate=new Date,a.isActive=function(b){return 0===i.compare(b.date,i.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){j=a,j.$render=function(){i.render()}},this.render=function(){if(j.$viewValue){var a=new Date(j.$viewValue),b=!isNaN(a);b?this.activeDate=a:h||e.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=j.$viewValue?new Date(j.$viewValue):null;j.$setValidity("dateDisabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=j.$viewValue?new Date(j.$viewValue):null;return{date:a,label:f(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date),customClass:this.customClass(a)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.customClass=function(b){return a.customClass({date:b,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},this.fixTimeZone=function(a){var b=a.getHours();a.setHours(23===b?b+2:0)},a.select=function(b){if(a.datepickerMode===i.minMode){var c=j.$viewValue?new Date(j.$viewValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),j.$setViewValue(c),j.$render()}else i.activeDate=b,a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=i.activeDate.getFullYear()+a*(i.step.years||0),c=i.activeDate.getMonth()+a*(i.step.months||0);i.activeDate.setFullYear(b,c,1),i.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===i.maxMode&&1===b||a.datepickerMode===i.minMode&&-1===b||(a.datepickerMode=i.modes[i.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var k=function(){i.element[0].focus()};a.$on("uib:datepicker.focus",k),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),i.shortcutPropagation||b.stopPropagation(),"enter"===c||"space"===c){if(i.isDisabled(i.activeDate))return;a.select(i.activeDate)}else!b.ctrlKey||"up"!==c&&"down"!==c?(i.handleKeyDown(c,b),i.refreshView()):a.toggleMode("up"===c?1:-1)}}]).controller("UibDaypickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?f[b]:29}function e(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}var f=[31,28,31,30,31,30,31,31,30,31,30,31];this.step={months:1},this.element=b,this.init=function(b){angular.extend(b,this),a.showWeeks=b.showWeeks,b.refreshView()},this.getDates=function(a,b){for(var c,d=new Array(b),e=new Date(a),f=0;b>f;)c=new Date(e),this.fixTimeZone(c),d[f++]=c,e.setDate(e.getDate()+1);return d},this._refreshView=function(){var b=this.activeDate.getFullYear(),d=this.activeDate.getMonth(),f=new Date(b,d,1),g=this.startingDay-f.getDay(),h=g>0?7-g:-g,i=new Date(f);h>0&&i.setDate(-h+1);for(var j=this.getDates(i,42),k=0;42>k;k++)j[k]=angular.extend(this.createDateObject(j[k],this.formatDay),{secondary:j[k].getMonth()!==d,uid:a.uniqueId+"-"+k});a.labels=new Array(7);for(var l=0;7>l;l++)a.labels[l]={abbr:c(j[l].date,this.formatDayHeader),full:c(j[l].date,"EEEE")};if(a.title=c(this.activeDate,this.formatDayTitle),a.rows=this.split(j,7),a.showWeeks){a.weekNumbers=[];for(var m=(11-this.startingDay)%7,n=a.rows.length,o=0;n>o;o++)a.weekNumbers.push(e(a.rows[o][m].date))}},this.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},this.handleKeyDown=function(a,b){var c=this.activeDate.getDate();if("left"===a)c-=1;else if("up"===a)c-=7;else if("right"===a)c+=1;else if("down"===a)c+=7;else if("pageup"===a||"pagedown"===a){var e=this.activeDate.getMonth()+("pageup"===a?-1:1);this.activeDate.setMonth(e,1),c=Math.min(d(this.activeDate.getFullYear(),this.activeDate.getMonth()),c)}else"home"===a?c=1:"end"===a&&(c=d(this.activeDate.getFullYear(),this.activeDate.getMonth()));this.activeDate.setDate(c)}}]).controller("UibMonthpickerController",["$scope","$element","dateFilter",function(a,b,c){this.step={years:1},this.element=b,this.init=function(a){angular.extend(a,this),a.refreshView()},this._refreshView=function(){for(var b,d=new Array(12),e=this.activeDate.getFullYear(),f=0;12>f;f++)b=new Date(e,f,1),this.fixTimeZone(b),d[f]=angular.extend(this.createDateObject(b,this.formatMonth),{uid:a.uniqueId+"-"+f});a.title=c(this.activeDate,this.formatMonthTitle),a.rows=this.split(d,3)},this.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},this.handleKeyDown=function(a,b){var c=this.activeDate.getMonth();if("left"===a)c-=1;else if("up"===a)c-=3;else if("right"===a)c+=1;else if("down"===a)c+=3;else if("pageup"===a||"pagedown"===a){var d=this.activeDate.getFullYear()+("pageup"===a?-1:1);this.activeDate.setFullYear(d)}else"home"===a?c=0:"end"===a&&(c=11);this.activeDate.setMonth(c)}}]).controller("UibYearpickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a){return parseInt((a-1)/e,10)*e+1}var e;this.element=b,this.yearpickerInit=function(){e=this.yearRange,this.step={years:e}},this._refreshView=function(){for(var b,c=new Array(e),f=0,g=d(this.activeDate.getFullYear());e>f;f++)b=new Date(g+f,0,1),this.fixTimeZone(b),c[f]=angular.extend(this.createDateObject(b,this.formatYear),{uid:a.uniqueId+"-"+f});a.title=[c[0].label,c[e-1].label].join(" - "),a.rows=this.split(c,5)},this.compare=function(a,b){return a.getFullYear()-b.getFullYear()},this.handleKeyDown=function(a,b){var c=this.activeDate.getFullYear();"left"===a?c-=1:"up"===a?c-=5:"right"===a?c+=1:"down"===a?c+=5:"pageup"===a||"pagedown"===a?c+=("pageup"===a?-1:1)*this.step.years:"home"===a?c=d(this.activeDate.getFullYear()):"end"===a&&(c=d(this.activeDate.getFullYear())+e-1),this.activeDate.setFullYear(c)}}]).directive("uibDatepicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"template/datepicker/datepicker.html"},scope:{datepickerMode:"=?",dateDisabled:"&",customClass:"&",shortcutPropagation:"&?"},require:["uibDatepicker","^ngModel"],controller:"UibDatepickerController",controllerAs:"datepicker",link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}).directive("uibDaypicker",function(){return{replace:!0,templateUrl:"template/datepicker/day.html",require:["^?uibDatepicker","uibDaypicker","^?datepicker"],controller:"UibDaypickerController",link:function(a,b,c,d){var e=d[0]||d[2],f=d[1];f.init(e)}}}).directive("uibMonthpicker",function(){return{replace:!0,templateUrl:"template/datepicker/month.html",require:["^?uibDatepicker","uibMonthpicker","^?datepicker"],controller:"UibMonthpickerController",link:function(a,b,c,d){var e=d[0]||d[2],f=d[1];f.init(e)}}}).directive("uibYearpicker",function(){return{replace:!0,templateUrl:"template/datepicker/year.html",require:["^?uibDatepicker","uibYearpicker","^?datepicker"],controller:"UibYearpickerController",link:function(a,b,c,d){var e=d[0]||d[2];angular.extend(e,d[1]),e.yearpickerInit(),e.refreshView()}}}).constant("uibDatepickerPopupConfig",{datepickerPopup:"yyyy-MM-dd",datepickerPopupTemplateUrl:"template/datepicker/popup.html",datepickerTemplateUrl:"template/datepicker/datepicker.html",html5Types:{date:"yyyy-MM-dd","datetime-local":"yyyy-MM-ddTHH:mm:ss.sss",month:"yyyy-MM"},currentText:"Today",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0,onOpenFocus:!0}).controller("UibDatepickerPopupController",["$scope","$element","$attrs","$compile","$parse","$document","$rootScope","$uibPosition","dateFilter","uibDateParser","uibDatepickerPopupConfig","$timeout",function(a,b,c,d,e,f,g,h,i,j,k,l){
8 function m(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function n(b){if(angular.isNumber(b)&&(b=new Date(b)),b){if(angular.isDate(b)&&!isNaN(b))return b;if(angular.isString(b)){var c=j.parse(b,r,a.date);return isNaN(c)?void 0:c}return void 0}return null}function o(a,b){var d=a||b;if(!c.ngRequired&&!d)return!0;if(angular.isNumber(d)&&(d=new Date(d)),d){if(angular.isDate(d)&&!isNaN(d))return!0;if(angular.isString(d)){var e=j.parse(d,r);return!isNaN(e)}return!1}return!0}function p(c){var d=A[0],e=b[0].contains(c.target),f=void 0!==d.contains&&d.contains(c.target);!a.isOpen||e||f||a.$apply(function(){a.isOpen=!1})}function q(c){27===c.which&&a.isOpen?(c.preventDefault(),c.stopPropagation(),a.$apply(function(){a.isOpen=!1}),b[0].focus()):40!==c.which||a.isOpen||(c.preventDefault(),c.stopPropagation(),a.$apply(function(){a.isOpen=!0}))}var r,s,t,u,v,w,x,y,z,A,B={},C=!1;a.watchData={},this.init=function(h){if(z=h,s=angular.isDefined(c.closeOnDateSelection)?a.$parent.$eval(c.closeOnDateSelection):k.closeOnDateSelection,t=angular.isDefined(c.datepickerAppendToBody)?a.$parent.$eval(c.datepickerAppendToBody):k.appendToBody,u=angular.isDefined(c.onOpenFocus)?a.$parent.$eval(c.onOpenFocus):k.onOpenFocus,v=angular.isDefined(c.datepickerPopupTemplateUrl)?c.datepickerPopupTemplateUrl:k.datepickerPopupTemplateUrl,w=angular.isDefined(c.datepickerTemplateUrl)?c.datepickerTemplateUrl:k.datepickerTemplateUrl,a.showButtonBar=angular.isDefined(c.showButtonBar)?a.$parent.$eval(c.showButtonBar):k.showButtonBar,k.html5Types[c.type]?(r=k.html5Types[c.type],C=!0):(r=c.datepickerPopup||k.datepickerPopup,c.$observe("uibDatepickerPopup",function(a,b){var c=a||k.datepickerPopup;if(c!==r&&(r=c,z.$modelValue=null,!r))throw new Error("uibDatepickerPopup must have a date format specified.")})),!r)throw new Error("uibDatepickerPopup must have a date format specified.");if(C&&c.datepickerPopup)throw new Error("HTML5 date input types do not support custom formats.");if(x=angular.element("<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>"),x.attr({"ng-model":"date","ng-change":"dateSelection(date)","template-url":v}),y=angular.element(x.children()[0]),y.attr("template-url",w),C&&"month"===c.type&&(y.attr("datepicker-mode",'"month"'),y.attr("min-mode","month")),c.datepickerOptions){var l=a.$parent.$eval(c.datepickerOptions);l&&l.initDate&&(a.initDate=l.initDate,y.attr("init-date","initDate"),delete l.initDate),angular.forEach(l,function(a,b){y.attr(m(b),a)})}angular.forEach(["minMode","maxMode","minDate","maxDate","datepickerMode","initDate","shortcutPropagation"],function(b){if(c[b]){var d=e(c[b]);if(a.$parent.$watch(d,function(c){a.watchData[b]=c,("minDate"===b||"maxDate"===b)&&(B[b]=new Date(c))}),y.attr(m(b),"watchData."+b),"datepickerMode"===b){var f=d.assign;a.$watch("watchData."+b,function(b,c){angular.isFunction(f)&&b!==c&&f(a.$parent,b)})}}}),c.dateDisabled&&y.attr("date-disabled","dateDisabled({ date: date, mode: mode })"),c.showWeeks&&y.attr("show-weeks",c.showWeeks),c.customClass&&y.attr("custom-class","customClass({ date: date, mode: mode })"),C?z.$formatters.push(function(b){return a.date=b,b}):(z.$$parserName="date",z.$validators.date=o,z.$parsers.unshift(n),z.$formatters.push(function(b){return a.date=b,z.$isEmpty(b)?b:i(b,r)})),z.$viewChangeListeners.push(function(){a.date=j.parse(z.$viewValue,r,a.date)}),b.bind("keydown",q),A=d(x)(a),x.remove(),t?f.find("body").append(A):b.after(A),a.$on("$destroy",function(){a.isOpen===!0&&(g.$$phase||a.$apply(function(){a.isOpen=!1})),A.remove(),b.unbind("keydown",q),f.unbind("click",p)})},a.getText=function(b){return a[b+"Text"]||k[b+"Text"]},a.isDisabled=function(b){return"today"===b&&(b=new Date),a.watchData.minDate&&a.compare(b,B.minDate)<0||a.watchData.maxDate&&a.compare(b,B.maxDate)>0},a.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},a.dateSelection=function(c){angular.isDefined(c)&&(a.date=c);var d=a.date?i(a.date,r):null;b.val(d),z.$setViewValue(d),s&&(a.isOpen=!1,b[0].focus())},a.keydown=function(c){27===c.which&&(a.isOpen=!1,b[0].focus())},a.select=function(b){if("today"===b){var c=new Date;angular.isDate(a.date)?(b=new Date(a.date),b.setFullYear(c.getFullYear(),c.getMonth(),c.getDate())):b=new Date(c.setHours(0,0,0,0))}a.dateSelection(b)},a.close=function(){a.isOpen=!1,b[0].focus()},a.$watch("isOpen",function(c){c?(a.position=t?h.offset(b):h.position(b),a.position.top=a.position.top+b.prop("offsetHeight"),l(function(){u&&a.$broadcast("uib:datepicker.focus"),f.bind("click",p)},0,!1)):f.unbind("click",p)})}]).directive("uibDatepickerPopup",function(){return{require:["ngModel","uibDatepickerPopup"],controller:"UibDatepickerPopupController",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&",customClass:"&"},link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibDatepickerPopupWrap",function(){return{replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"template/datepicker/popup.html"}}}),angular.module("ui.bootstrap.datepicker").value("$datepickerSuppressWarning",!1).controller("DatepickerController",["$scope","$attrs","$parse","$interpolate","$log","dateFilter","uibDatepickerConfig","$datepickerSuppressError","$datepickerSuppressWarning",function(a,b,c,d,e,f,g,h,i){i||e.warn("DatepickerController is now deprecated. Use UibDatepickerController instead.");var j=this,k={$setViewValue:angular.noop};this.modes=["day","month","year"],angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","showWeeks","startingDay","yearRange","shortcutPropagation"],function(c,e){j[c]=angular.isDefined(b[c])?6>e?d(b[c])(a.$parent):a.$parent.$eval(b[c]):g[c]}),angular.forEach(["minDate","maxDate"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(a){j[d]=a?new Date(a):null,j.refreshView()}):j[d]=g[d]?new Date(g[d]):null}),angular.forEach(["minMode","maxMode"],function(d){b[d]?a.$parent.$watch(c(b[d]),function(c){j[d]=angular.isDefined(c)?c:b[d],a[d]=j[d],("minMode"==d&&j.modes.indexOf(a.datepickerMode)<j.modes.indexOf(j[d])||"maxMode"==d&&j.modes.indexOf(a.datepickerMode)>j.modes.indexOf(j[d]))&&(a.datepickerMode=j[d])}):(j[d]=g[d]||null,a[d]=j[d])}),a.datepickerMode=a.datepickerMode||g.datepickerMode,a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),angular.isDefined(b.initDate)?(this.activeDate=a.$parent.$eval(b.initDate)||new Date,a.$parent.$watch(b.initDate,function(a){a&&(k.$isEmpty(k.$modelValue)||k.$invalid)&&(j.activeDate=a,j.refreshView())})):this.activeDate=new Date,a.isActive=function(b){return 0===j.compare(b.date,j.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){k=a,k.$render=function(){j.render()}},this.render=function(){if(k.$viewValue){var a=new Date(k.$viewValue),b=!isNaN(a);b?this.activeDate=a:h||e.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')}this.refreshView()},this.refreshView=function(){if(this.element){this._refreshView();var a=k.$viewValue?new Date(k.$viewValue):null;k.$setValidity("dateDisabled",!a||this.element&&!this.isDisabled(a))}},this.createDateObject=function(a,b){var c=k.$viewValue?new Date(k.$viewValue):null;return{date:a,label:f(a,b),selected:c&&0===this.compare(a,c),disabled:this.isDisabled(a),current:0===this.compare(a,new Date),customClass:this.customClass(a)}},this.isDisabled=function(c){return this.minDate&&this.compare(c,this.minDate)<0||this.maxDate&&this.compare(c,this.maxDate)>0||b.dateDisabled&&a.dateDisabled({date:c,mode:a.datepickerMode})},this.customClass=function(b){return a.customClass({date:b,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c},this.fixTimeZone=function(a){var b=a.getHours();a.setHours(23===b?b+2:0)},a.select=function(b){if(a.datepickerMode===j.minMode){var c=k.$viewValue?new Date(k.$viewValue):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),k.$setViewValue(c),k.$render()}else j.activeDate=b,a.datepickerMode=j.modes[j.modes.indexOf(a.datepickerMode)-1]},a.move=function(a){var b=j.activeDate.getFullYear()+a*(j.step.years||0),c=j.activeDate.getMonth()+a*(j.step.months||0);j.activeDate.setFullYear(b,c,1),j.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===j.maxMode&&1===b||a.datepickerMode===j.minMode&&-1===b||(a.datepickerMode=j.modes[j.modes.indexOf(a.datepickerMode)+b])},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var l=function(){j.element[0].focus()};a.$on("uib:datepicker.focus",l),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey)if(b.preventDefault(),j.shortcutPropagation||b.stopPropagation(),"enter"===c||"space"===c){if(j.isDisabled(j.activeDate))return;a.select(j.activeDate)}else!b.ctrlKey||"up"!==c&&"down"!==c?(j.handleKeyDown(c,b),j.refreshView()):a.toggleMode("up"===c?1:-1)}}]).directive("datepicker",["$log","$datepickerSuppressWarning",function(a,b){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"template/datepicker/datepicker.html"},scope:{datepickerMode:"=?",dateDisabled:"&",customClass:"&",shortcutPropagation:"&?"},require:["datepicker","^ngModel"],controller:"DatepickerController",controllerAs:"datepicker",link:function(c,d,e,f){b||a.warn("datepicker is now deprecated. Use uib-datepicker instead.");var g=f[0],h=f[1];g.init(h)}}}]).directive("daypicker",["$log","$datepickerSuppressWarning",function(a,b){return{replace:!0,templateUrl:"template/datepicker/day.html",require:["^datepicker","daypicker"],controller:"UibDaypickerController",link:function(c,d,e,f){b||a.warn("daypicker is now deprecated. Use uib-daypicker instead.");var g=f[0],h=f[1];h.init(g)}}}]).directive("monthpicker",["$log","$datepickerSuppressWarning",function(a,b){return{replace:!0,templateUrl:"template/datepicker/month.html",require:["^datepicker","monthpicker"],controller:"UibMonthpickerController",link:function(c,d,e,f){b||a.warn("monthpicker is now deprecated. Use uib-monthpicker instead.");var g=f[0],h=f[1];h.init(g)}}}]).directive("yearpicker",["$log","$datepickerSuppressWarning",function(a,b){return{replace:!0,templateUrl:"template/datepicker/year.html",require:["^datepicker","yearpicker"],controller:"UibYearpickerController",link:function(c,d,e,f){b||a.warn("yearpicker is now deprecated. Use uib-yearpicker instead.");var g=f[0];angular.extend(g,f[1]),g.yearpickerInit(),g.refreshView()}}}]).directive("datepickerPopup",["$log","$datepickerSuppressWarning",function(a,b){return{require:["ngModel","datepickerPopup"],controller:"UibDatepickerPopupController",scope:{isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&",customClass:"&"},link:function(c,d,e,f){b||a.warn("datepicker-popup is now deprecated. Use uib-datepicker-popup instead.");var g=f[0],h=f[1];h.init(g)}}}]).directive("datepickerPopupWrap",["$log","$datepickerSuppressWarning",function(a,b){return{replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"template/datepicker/popup.html"},link:function(){b||a.warn("datepicker-popup-wrap is now deprecated. Use uib-datepicker-popup-wrap instead.")}}}]),angular.module("ui.bootstrap.dropdown",["ui.bootstrap.position"]).constant("uibDropdownConfig",{openClass:"open"}).service("uibDropdownService",["$document","$rootScope",function(a,b){var c=null;this.open=function(b){c||(a.bind("click",d),a.bind("keydown",e)),c&&c!==b&&(c.isOpen=!1),c=b},this.close=function(b){c===b&&(c=null,a.unbind("click",d),a.unbind("keydown",e))};var d=function(a){if(c&&(!a||"disabled"!==c.getAutoClose())){var d=c.getToggleElement();if(!(a&&d&&d[0].contains(a.target))){var e=c.getDropdownElement();a&&"outsideClick"===c.getAutoClose()&&e&&e[0].contains(a.target)||(c.isOpen=!1,b.$$phase||c.$apply())}}},e=function(a){27===a.which?(c.focusToggleElement(),d()):c.isKeynavEnabled()&&/(38|40)/.test(a.which)&&c.isOpen&&(a.preventDefault(),a.stopPropagation(),c.focusDropdownEntry(a.which))}}]).controller("UibDropdownController",["$scope","$element","$attrs","$parse","uibDropdownConfig","uibDropdownService","$animate","$uibPosition","$document","$compile","$templateRequest",function(a,b,c,d,e,f,g,h,i,j,k){var l,m,n=this,o=a.$new(),p=e.openClass,q=angular.noop,r=c.onToggle?d(c.onToggle):angular.noop,s=!1,t=!1;b.addClass("dropdown"),this.init=function(){c.isOpen&&(m=d(c.isOpen),q=m.assign,a.$watch(m,function(a){o.isOpen=!!a})),s=angular.isDefined(c.dropdownAppendToBody),t=angular.isDefined(c.uibKeyboardNav),s&&n.dropdownMenu&&(i.find("body").append(n.dropdownMenu),b.on("$destroy",function(){n.dropdownMenu.remove()}))},this.toggle=function(a){return o.isOpen=arguments.length?!!a:!o.isOpen},this.isOpen=function(){return o.isOpen},o.getToggleElement=function(){return n.toggleElement},o.getAutoClose=function(){return c.autoClose||"always"},o.getElement=function(){return b},o.isKeynavEnabled=function(){return t},o.focusDropdownEntry=function(a){var c=n.dropdownMenu?angular.element(n.dropdownMenu).find("a"):angular.element(b).find("ul").eq(0).find("a");switch(a){case 40:angular.isNumber(n.selectedOption)?n.selectedOption=n.selectedOption===c.length-1?n.selectedOption:n.selectedOption+1:n.selectedOption=0;break;case 38:angular.isNumber(n.selectedOption)?n.selectedOption=0===n.selectedOption?0:n.selectedOption-1:n.selectedOption=c.length-1}c[n.selectedOption].focus()},o.getDropdownElement=function(){return n.dropdownMenu},o.focusToggleElement=function(){n.toggleElement&&n.toggleElement[0].focus()},o.$watch("isOpen",function(c,d){if(s&&n.dropdownMenu){var e=h.positionElements(b,n.dropdownMenu,"bottom-left",!0),i={top:e.top+"px",display:c?"block":"none"},m=n.dropdownMenu.hasClass("dropdown-menu-right");m?(i.left="auto",i.right=window.innerWidth-(e.left+b.prop("offsetWidth"))+"px"):(i.left=e.left+"px",i.right="auto"),n.dropdownMenu.css(i)}if(g[c?"addClass":"removeClass"](b,p).then(function(){angular.isDefined(c)&&c!==d&&r(a,{open:!!c})}),c)n.dropdownMenuTemplateUrl&&k(n.dropdownMenuTemplateUrl).then(function(a){l=o.$new(),j(a.trim())(l,function(a){var b=a;n.dropdownMenu.replaceWith(b),n.dropdownMenu=b})}),o.focusToggleElement(),f.open(o);else{if(n.dropdownMenuTemplateUrl){l&&l.$destroy();var t=angular.element('<ul class="dropdown-menu"></ul>');n.dropdownMenu.replaceWith(t),n.dropdownMenu=t}f.close(o),n.selectedOption=null}angular.isFunction(q)&&q(a,c)}),a.$on("$locationChangeSuccess",function(){"disabled"!==o.getAutoClose()&&(o.isOpen=!1)});var u=a.$on("$destroy",function(){o.$destroy()});o.$on("$destroy",u)}]).directive("uibDropdown",function(){return{controller:"UibDropdownController",link:function(a,b,c,d){d.init()}}}).directive("uibDropdownMenu",function(){return{restrict:"AC",require:"?^uibDropdown",link:function(a,b,c,d){if(d&&!angular.isDefined(c.dropdownNested)){b.addClass("dropdown-menu");var e=c.templateUrl;e&&(d.dropdownMenuTemplateUrl=e),d.dropdownMenu||(d.dropdownMenu=b)}}}}).directive("uibKeyboardNav",function(){return{restrict:"A",require:"?^uibDropdown",link:function(a,b,c,d){b.bind("keydown",function(a){if(-1!==[38,40].indexOf(a.which)){a.preventDefault(),a.stopPropagation();var b=d.dropdownMenu.find("a");switch(a.which){case 40:angular.isNumber(d.selectedOption)?d.selectedOption=d.selectedOption===b.length-1?d.selectedOption:d.selectedOption+1:d.selectedOption=0;break;case 38:angular.isNumber(d.selectedOption)?d.selectedOption=0===d.selectedOption?0:d.selectedOption-1:d.selectedOption=b.length-1}b[d.selectedOption].focus()}})}}}).directive("uibDropdownToggle",function(){return{require:"?^uibDropdown",link:function(a,b,c,d){if(d){b.addClass("dropdown-toggle"),d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.dropdown").value("$dropdownSuppressWarning",!1).service("dropdownService",["$log","$dropdownSuppressWarning","uibDropdownService",function(a,b,c){b||a.warn("dropdownService is now deprecated. Use uibDropdownService instead."),angular.extend(this,c)}]).controller("DropdownController",["$scope","$element","$attrs","$parse","uibDropdownConfig","uibDropdownService","$animate","$uibPosition","$document","$compile","$templateRequest","$log","$dropdownSuppressWarning",function(a,b,c,d,e,f,g,h,i,j,k,l,m){m||l.warn("DropdownController is now deprecated. Use UibDropdownController instead.");var n,o,p=this,q=a.$new(),r=e.openClass,s=angular.noop,t=c.onToggle?d(c.onToggle):angular.noop,u=!1,v=!1;b.addClass("dropdown"),this.init=function(){c.isOpen&&(o=d(c.isOpen),s=o.assign,a.$watch(o,function(a){q.isOpen=!!a})),u=angular.isDefined(c.dropdownAppendToBody),v=angular.isDefined(c.uibKeyboardNav),u&&p.dropdownMenu&&(i.find("body").append(p.dropdownMenu),b.on("$destroy",function(){p.dropdownMenu.remove()}))},this.toggle=function(a){return q.isOpen=arguments.length?!!a:!q.isOpen},this.isOpen=function(){return q.isOpen},q.getToggleElement=function(){return p.toggleElement},q.getAutoClose=function(){return c.autoClose||"always"},q.getElement=function(){return b},q.isKeynavEnabled=function(){return v},q.focusDropdownEntry=function(a){var c=p.dropdownMenu?angular.element(p.dropdownMenu).find("a"):angular.element(b).find("ul").eq(0).find("a");switch(a){case 40:angular.isNumber(p.selectedOption)?p.selectedOption=p.selectedOption===c.length-1?p.selectedOption:p.selectedOption+1:p.selectedOption=0;break;case 38:angular.isNumber(p.selectedOption)?p.selectedOption=0===p.selectedOption?0:p.selectedOption-1:p.selectedOption=c.length-1}c[p.selectedOption].focus()},q.getDropdownElement=function(){return p.dropdownMenu},q.focusToggleElement=function(){p.toggleElement&&p.toggleElement[0].focus()},q.$watch("isOpen",function(c,d){if(u&&p.dropdownMenu){var e=h.positionElements(b,p.dropdownMenu,"bottom-left",!0),i={top:e.top+"px",display:c?"block":"none"},l=p.dropdownMenu.hasClass("dropdown-menu-right");l?(i.left="auto",i.right=window.innerWidth-(e.left+b.prop("offsetWidth"))+"px"):(i.left=e.left+"px",i.right="auto"),p.dropdownMenu.css(i)}if(g[c?"addClass":"removeClass"](b,r).then(function(){angular.isDefined(c)&&c!==d&&t(a,{open:!!c})}),c)p.dropdownMenuTemplateUrl&&k(p.dropdownMenuTemplateUrl).then(function(a){n=q.$new(),j(a.trim())(n,function(a){var b=a;p.dropdownMenu.replaceWith(b),p.dropdownMenu=b})}),q.focusToggleElement(),f.open(q);else{if(p.dropdownMenuTemplateUrl){n&&n.$destroy();var m=angular.element('<ul class="dropdown-menu"></ul>');p.dropdownMenu.replaceWith(m),p.dropdownMenu=m}f.close(q),p.selectedOption=null}angular.isFunction(s)&&s(a,c)}),a.$on("$locationChangeSuccess",function(){"disabled"!==q.getAutoClose()&&(q.isOpen=!1)});var w=a.$on("$destroy",function(){q.$destroy()});q.$on("$destroy",w)}]).directive("dropdown",["$log","$dropdownSuppressWarning",function(a,b){return{controller:"DropdownController",link:function(c,d,e,f){b||a.warn("dropdown is now deprecated. Use uib-dropdown instead."),f.init()}}}]).directive("dropdownMenu",["$log","$dropdownSuppressWarning",function(a,b){return{restrict:"AC",require:"?^dropdown",link:function(c,d,e,f){if(f){b||a.warn("dropdown-menu is now deprecated. Use uib-dropdown-menu instead."),d.addClass("dropdown-menu");var g=e.templateUrl;g&&(f.dropdownMenuTemplateUrl=g),f.dropdownMenu||(f.dropdownMenu=d)}}}}]).directive("keyboardNav",["$log","$dropdownSuppressWarning",function(a,b){return{restrict:"A",require:"?^dropdown",link:function(c,d,e,f){b||a.warn("keyboard-nav is now deprecated. Use uib-keyboard-nav instead."),d.bind("keydown",function(a){if(-1!==[38,40].indexOf(a.which)){a.preventDefault(),a.stopPropagation();var b=f.dropdownMenu.find("a");switch(a.which){case 40:angular.isNumber(f.selectedOption)?f.selectedOption=f.selectedOption===b.length-1?f.selectedOption:f.selectedOption+1:f.selectedOption=0;break;case 38:angular.isNumber(f.selectedOption)?f.selectedOption=0===f.selectedOption?0:f.selectedOption-1:f.selectedOption=b.length-1}b[f.selectedOption].focus()}})}}}]).directive("dropdownToggle",["$log","$dropdownSuppressWarning",function(a,b){return{require:"?^dropdown",link:function(c,d,e,f){if(b||a.warn("dropdown-toggle is now deprecated. Use uib-dropdown-toggle instead."),f){d.addClass("dropdown-toggle"),f.toggleElement=d;var g=function(a){a.preventDefault(),d.hasClass("disabled")||e.disabled||c.$apply(function(){f.toggle()})};d.bind("click",g),d.attr({"aria-haspopup":!0,"aria-expanded":!1}),c.$watch(f.isOpen,function(a){d.attr("aria-expanded",!!a)}),c.$on("$destroy",function(){d.unbind("click",g)})}}}}]),angular.module("ui.bootstrap.stackedMap",[]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c<a.length;c++)if(b==a[c].key)return a[c]},keys:function(){for(var b=[],c=0;c<a.length;c++)b.push(a[c].key);return b},top:function(){return a[a.length-1]},remove:function(b){for(var c=-1,d=0;d<a.length;d++)if(b==a[d].key){c=d;break}return a.splice(c,1)[0]},removeTop:function(){return a.splice(a.length-1,1)[0]},length:function(){return a.length}}}}}),angular.module("ui.bootstrap.modal",["ui.bootstrap.stackedMap"]).factory("$$multiMap",function(){return{createNew:function(){var a={};return{entries:function(){return Object.keys(a).map(function(b){return{key:b,value:a[b]}})},get:function(b){return a[b]},hasKey:function(b){return!!a[b]},keys:function(){return Object.keys(a)},put:function(b,c){a[b]||(a[b]=[]),a[b].push(c)},remove:function(b,c){var d=a[b];if(d){var e=d.indexOf(c);-1!==e&&d.splice(e,1),d.length||delete a[b]}}}}}}).directive("uibModalBackdrop",["$animate","$injector","$uibModalStack",function(a,b,c){function d(b,d,f){d.addClass("modal-backdrop"),f.modalInClass&&(e?e(d,{addClass:f.modalInClass}).start():a.addClass(d,f.modalInClass),b.$on(c.NOW_CLOSING_EVENT,function(b,c){var g=c();e?e(d,{removeClass:f.modalInClass}).start().then(g):a.removeClass(d,f.modalInClass).then(g)}))}var e=null;return b.has("$animateCss")&&(e=b.get("$animateCss")),{replace:!0,templateUrl:"template/modal/backdrop.html",compile:function(a,b){return a.addClass(b.backdropClass),d}}}]).directive("uibModalWindow",["$uibModalStack","$q","$animate","$injector",function(a,b,c,d){var e=null;return d.has("$animateCss")&&(e=d.get("$animateCss")),{scope:{index:"@"},replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"template/modal/window.html"},link:function(d,f,g){f.addClass(g.windowClass||""),f.addClass(g.windowTopClass||""),d.size=g.size,d.close=function(b){var c=a.getTop();c&&c.value.backdrop&&"static"!==c.value.backdrop&&b.target===b.currentTarget&&(b.preventDefault(),b.stopPropagation(),a.dismiss(c.key,"backdrop click"))},f.on("click",d.close),d.$isRendered=!0;var h=b.defer();g.$observe("modalRender",function(a){"true"==a&&h.resolve()}),h.promise.then(function(){var h=null;g.modalInClass&&(h=e?e(f,{addClass:g.modalInClass}).start():c.addClass(f,g.modalInClass),d.$on(a.NOW_CLOSING_EVENT,function(a,b){var d=b();e?e(f,{removeClass:g.modalInClass}).start().then(d):c.removeClass(f,g.modalInClass).then(d)})),b.when(h).then(function(){var a=f[0].querySelectorAll("[autofocus]");a.length?a[0].focus():f[0].focus()});var i=a.getTop();i&&a.modalRendered(i.key)})}}}]).directive("uibModalAnimationClass",[function(){return{compile:function(a,b){b.modalAnimation&&a.addClass(b.uibModalAnimationClass)}}}]).directive("uibModalTransclude",function(){return{link:function(a,b,c,d,e){e(a.$parent,function(a){b.empty(),b.append(a)})}}}).factory("$uibModalStack",["$animate","$timeout","$document","$compile","$rootScope","$q","$injector","$$multiMap","$$stackedMap",function(a,b,c,d,e,f,g,h,i){function j(){for(var a=-1,b=u.keys(),c=0;c<b.length;c++)u.get(b[c]).value.backdrop&&(a=c);return a}function k(a,b){var d=c.find("body").eq(0),e=u.get(a).value;u.remove(a),n(e.modalDomEl,e.modalScope,function(){var b=e.openedClass||t;v.remove(b,a),d.toggleClass(b,v.hasKey(b)),l(!0)}),m(),b&&b.focus?b.focus():d.focus()}function l(a){var b;u.length()>0&&(b=u.top().value,b.modalDomEl.toggleClass(b.windowTopClass||"",a))}function m(){if(q&&-1==j()){var a=r;n(q,r,function(){a=null}),q=void 0,r=void 0}}function n(b,c,d){function e(){e.done||(e.done=!0,p?p(b,{event:"leave"}).start().then(function(){b.remove()}):a.leave(b),c.$destroy(),d&&d())}var g,h=null,i=function(){return g||(g=f.defer(),h=g.promise),function(){g.resolve()}};return c.$broadcast(w.NOW_CLOSING_EVENT,i),f.when(h).then(e)}function o(a,b,c){return!a.value.modalScope.$broadcast("modal.closing",b,c).defaultPrevented}var p=null;g.has("$animateCss")&&(p=g.get("$animateCss"));var q,r,s,t="modal-open",u=i.createNew(),v=h.createNew(),w={NOW_CLOSING_EVENT:"modal.stack.now-closing"},x=0,y="a[href], area[href], input:not([disabled]), button:not([disabled]),select:not([disabled]), textarea:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable=true]";return e.$watch(j,function(a){r&&(r.index=a)}),c.bind("keydown",function(a){if(a.isDefaultPrevented())return a;var b=u.top();if(b&&b.value.keyboard)switch(a.which){case 27:a.preventDefault(),e.$apply(function(){w.dismiss(b.key,"escape key press")});break;case 9:w.loadFocusElementList(b);var c=!1;a.shiftKey?w.isFocusInFirstItem(a)&&(c=w.focusLastFocusableElement()):w.isFocusInLastItem(a)&&(c=w.focusFirstFocusableElement()),c&&(a.preventDefault(),a.stopPropagation())}}),w.open=function(a,b){var f=c[0].activeElement,g=b.openedClass||t;l(!1),u.add(a,{deferred:b.deferred,renderDeferred:b.renderDeferred,modalScope:b.scope,backdrop:b.backdrop,keyboard:b.keyboard,openedClass:b.openedClass,windowTopClass:b.windowTopClass}),v.put(g,a);var h=c.find("body").eq(0),i=j();if(i>=0&&!q){r=e.$new(!0),r.index=i;var k=angular.element('<div uib-modal-backdrop="modal-backdrop"></div>');k.attr("backdrop-class",b.backdropClass),b.animation&&k.attr("modal-animation","true"),q=d(k)(r),h.append(q)}var m=angular.element('<div uib-modal-window="modal-window"></div>');m.attr({"template-url":b.windowTemplateUrl,"window-class":b.windowClass,"window-top-class":b.windowTopClass,size:b.size,index:u.length()-1,animate:"animate"}).html(b.content),b.animation&&m.attr("modal-animation","true");var n=d(m)(b.scope);u.top().value.modalDomEl=n,u.top().value.modalOpener=f,h.append(n),h.addClass(g),w.clearFocusListCache()},w.close=function(a,b){var c=u.get(a);return c&&o(c,b,!0)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.resolve(b),k(a,c.value.modalOpener),!0):!c},w.dismiss=function(a,b){var c=u.get(a);return c&&o(c,b,!1)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.reject(b),k(a,c.value.modalOpener),!0):!c},w.dismissAll=function(a){for(var b=this.getTop();b&&this.dismiss(b.key,a);)b=this.getTop()},w.getTop=function(){return u.top()},w.modalRendered=function(a){var b=u.get(a);b&&b.value.renderDeferred.resolve()},w.focusFirstFocusableElement=function(){return s.length>0?(s[0].focus(),!0):!1},w.focusLastFocusableElement=function(){return s.length>0?(s[s.length-1].focus(),!0):!1},w.isFocusInFirstItem=function(a){return s.length>0?(a.target||a.srcElement)==s[0]:!1},w.isFocusInLastItem=function(a){return s.length>0?(a.target||a.srcElement)==s[s.length-1]:!1},w.clearFocusListCache=function(){s=[],x=0},w.loadFocusElementList=function(a){if((void 0===s||!s.length0)&&a){var b=a.value.modalDomEl;b&&b.length&&(s=b[0].querySelectorAll(y))}},w}]).provider("$uibModal",function(){var a={options:{animation:!0,backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$templateRequest","$controller","$uibModalStack",function(b,c,d,e,f,g){function h(a){return a.template?d.when(a.template):e(angular.isFunction(a.templateUrl)?a.templateUrl():a.templateUrl)}function i(a){var c=[];return angular.forEach(a,function(a){angular.isFunction(a)||angular.isArray(a)?c.push(d.when(b.invoke(a))):angular.isString(a)?c.push(d.when(b.get(a))):c.push(d.when(a))}),c}var j={},k=null;return j.getPromiseChain=function(){return k},j.open=function(b){var e=d.defer(),j=d.defer(),l=d.defer(),m={result:e.promise,opened:j.promise,rendered:l.promise,close:function(a){return g.close(m,a)},dismiss:function(a){return g.dismiss(m,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var n,o=d.all([h(b)].concat(i(b.resolve)));return n=k=d.all([k]).then(function(){return o},function(){return o}).then(function(a){var d=(b.scope||c).$new();d.$close=m.close,d.$dismiss=m.dismiss,d.$on("$destroy",function(){d.$$uibDestructionScheduled||d.$dismiss("$uibUnscheduledDestruction")});var h,i={},k=1;b.controller&&(i.$scope=d,i.$modalInstance=m,angular.forEach(b.resolve,function(b,c){i[c]=a[k++]}),h=f(b.controller,i),b.controllerAs&&(b.bindToController&&angular.extend(h,d),d[b.controllerAs]=h)),g.open(m,{scope:d,deferred:e,renderDeferred:l,content:a[0],animation:b.animation,backdrop:b.backdrop,keyboard:b.keyboard,backdropClass:b.backdropClass,windowTopClass:b.windowTopClass,windowClass:b.windowClass,windowTemplateUrl:b.windowTemplateUrl,size:b.size,openedClass:b.openedClass}),j.resolve(!0)},function(a){j.reject(a),e.reject(a)})["finally"](function(){k===n&&(k=null)}),m},j}]};return a}),angular.module("ui.bootstrap.modal").value("$modalSuppressWarning",!1).directive("modalBackdrop",["$animate","$injector","$modalStack","$log","$modalSuppressWarning",function(a,b,c,d,e){function f(b,f,h){e||d.warn("modal-backdrop is now deprecated. Use uib-modal-backdrop instead."),f.addClass("modal-backdrop"),h.modalInClass&&(g?g(f,{addClass:h.modalInClass}).start():a.addClass(f,h.modalInClass),b.$on(c.NOW_CLOSING_EVENT,function(b,c){var d=c();g?g(f,{removeClass:h.modalInClass}).start().then(d):a.removeClass(f,h.modalInClass).then(d)}))}var g=null;return b.has("$animateCss")&&(g=b.get("$animateCss")),{replace:!0,templateUrl:"template/modal/backdrop.html",compile:function(a,b){return a.addClass(b.backdropClass),f}}}]).directive("modalWindow",["$modalStack","$q","$animate","$injector","$log","$modalSuppressWarning",function(a,b,c,d,e,f){var g=null;return d.has("$animateCss")&&(g=d.get("$animateCss")),{scope:{index:"@"},replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"template/modal/window.html"},link:function(d,h,i){f||e.warn("modal-window is now deprecated. Use uib-modal-window instead."),h.addClass(i.windowClass||""),h.addClass(i.windowTopClass||""),d.size=i.size,d.close=function(b){var c=a.getTop();c&&c.value.backdrop&&"static"!==c.value.backdrop&&b.target===b.currentTarget&&(b.preventDefault(),b.stopPropagation(),a.dismiss(c.key,"backdrop click"))},h.on("click",d.close),d.$isRendered=!0;var j=b.defer();i.$observe("modalRender",function(a){"true"==a&&j.resolve()}),j.promise.then(function(){var e=null;i.modalInClass&&(e=g?g(h,{addClass:i.modalInClass}).start():c.addClass(h,i.modalInClass),d.$on(a.NOW_CLOSING_EVENT,function(a,b){var d=b();g?g(h,{removeClass:i.modalInClass}).start().then(d):c.removeClass(h,i.modalInClass).then(d)})),b.when(e).then(function(){var a=h[0].querySelectorAll("[autofocus]");a.length?a[0].focus():h[0].focus()});var f=a.getTop();f&&a.modalRendered(f.key)})}}}]).directive("modalAnimationClass",["$log","$modalSuppressWarning",function(a,b){return{compile:function(c,d){b||a.warn("modal-animation-class is now deprecated. Use uib-modal-animation-class instead."),d.modalAnimation&&c.addClass(d.modalAnimationClass)}}}]).directive("modalTransclude",["$log","$modalSuppressWarning",function(a,b){return{link:function(c,d,e,f,g){b||a.warn("modal-transclude is now deprecated. Use uib-modal-transclude instead."),g(c.$parent,function(a){d.empty(),d.append(a)})}}}]).service("$modalStack",["$animate","$timeout","$document","$compile","$rootScope","$q","$injector","$$multiMap","$$stackedMap","$uibModalStack","$log","$modalSuppressWarning",function(a,b,c,d,e,f,g,h,i,j,k,l){
9 l||k.warn("$modalStack is now deprecated. Use $uibModalStack instead."),angular.extend(this,j)}]).provider("$modal",["$uibModalProvider",function(a){angular.extend(this,a),this.$get=["$injector","$log","$modalSuppressWarning",function(b,c,d){return d||c.warn("$modal is now deprecated. Use $uibModal instead."),b.invoke(a.$get)}]}]),angular.module("ui.bootstrap.pagination",[]).controller("UibPaginationController",["$scope","$attrs","$parse",function(a,b,c){var d=this,e={$setViewValue:angular.noop},f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(g,h){e=g,this.config=h,e.$render=function(){d.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){d.itemsPerPage=parseInt(b,10),a.totalPages=d.calculateTotalPages()}):this.itemsPerPage=h.itemsPerPage,a.$watch("totalItems",function(){a.totalPages=d.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),a.page>b?a.selectPage(b):e.$render()})},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(e.$viewValue,10)||1},a.selectPage=function(b,c){c&&c.preventDefault();var d=!a.ngDisabled||!c;d&&a.page!==b&&b>0&&b<=a.totalPages&&(c&&c.target&&c.target.blur(),e.$setViewValue(b),e.$render())},a.getText=function(b){return a[b+"Text"]||d.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages}}]).constant("uibPaginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("uibPagination",["$parse","uibPaginationConfig",function(a,b){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@",ngDisabled:"="},require:["uibPagination","?ngModel"],controller:"UibPaginationController",controllerAs:"pagination",templateUrl:function(a,b){return b.templateUrl||"template/pagination/pagination.html"},replace:!0,link:function(c,d,e,f){function g(a,b,c){return{number:a,text:b,active:c}}function h(a,b){var c=[],d=1,e=b,f=angular.isDefined(k)&&b>k;f&&(l?(d=Math.max(a-Math.floor(k/2),1),e=d+k-1,e>b&&(e=b,d=e-k+1)):(d=(Math.ceil(a/k)-1)*k+1,e=Math.min(d+k-1,b)));for(var h=d;e>=h;h++){var i=g(h,h,h===a);c.push(i)}if(f&&!l){if(d>1){var j=g(d-1,"...",!1);c.unshift(j)}if(b>e){var m=g(e+1,"...",!1);c.push(m)}}return c}var i=f[0],j=f[1];if(j){var k=angular.isDefined(e.maxSize)?c.$parent.$eval(e.maxSize):b.maxSize,l=angular.isDefined(e.rotate)?c.$parent.$eval(e.rotate):b.rotate;c.boundaryLinks=angular.isDefined(e.boundaryLinks)?c.$parent.$eval(e.boundaryLinks):b.boundaryLinks,c.directionLinks=angular.isDefined(e.directionLinks)?c.$parent.$eval(e.directionLinks):b.directionLinks,i.init(j,b),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){k=parseInt(a,10),i.render()});var m=i.render;i.render=function(){m(),c.page>0&&c.page<=c.totalPages&&(c.pages=h(c.page,c.totalPages))}}}}}]).constant("uibPagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("uibPager",["uibPagerConfig",function(a){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["uibPager","?ngModel"],controller:"UibPaginationController",controllerAs:"pagination",templateUrl:function(a,b){return b.templateUrl||"template/pagination/pager.html"},replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&(b.align=angular.isDefined(d.align)?b.$parent.$eval(d.align):a.align,f.init(g,a))}}}]),angular.module("ui.bootstrap.pagination").value("$paginationSuppressWarning",!1).controller("PaginationController",["$scope","$attrs","$parse","$log","$paginationSuppressWarning",function(a,b,c,d,e){e||d.warn("PaginationController is now deprecated. Use UibPaginationController instead.");var f=this,g={$setViewValue:angular.noop},h=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d,e){g=d,this.config=e,g.$render=function(){f.render()},b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){f.itemsPerPage=parseInt(b,10),a.totalPages=f.calculateTotalPages()}):this.itemsPerPage=e.itemsPerPage,a.$watch("totalItems",function(){a.totalPages=f.calculateTotalPages()}),a.$watch("totalPages",function(b){h(a.$parent,b),a.page>b?a.selectPage(b):g.$render()})},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.render=function(){a.page=parseInt(g.$viewValue,10)||1},a.selectPage=function(b,c){c&&c.preventDefault();var d=!a.ngDisabled||!c;d&&a.page!==b&&b>0&&b<=a.totalPages&&(c&&c.target&&c.target.blur(),g.$setViewValue(b),g.$render())},a.getText=function(b){return a[b+"Text"]||f.config[b+"Text"]},a.noPrevious=function(){return 1===a.page},a.noNext=function(){return a.page===a.totalPages}}]).directive("pagination",["$parse","uibPaginationConfig","$log","$paginationSuppressWarning",function(a,b,c,d){return{restrict:"EA",scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@",ngDisabled:"="},require:["pagination","?ngModel"],controller:"PaginationController",controllerAs:"pagination",templateUrl:function(a,b){return b.templateUrl||"template/pagination/pagination.html"},replace:!0,link:function(e,f,g,h){function i(a,b,c){return{number:a,text:b,active:c}}function j(a,b){var c=[],d=1,e=b,f=angular.isDefined(m)&&b>m;f&&(n?(d=Math.max(a-Math.floor(m/2),1),e=d+m-1,e>b&&(e=b,d=e-m+1)):(d=(Math.ceil(a/m)-1)*m+1,e=Math.min(d+m-1,b)));for(var g=d;e>=g;g++){var h=i(g,g,g===a);c.push(h)}if(f&&!n){if(d>1){var j=i(d-1,"...",!1);c.unshift(j)}if(b>e){var k=i(e+1,"...",!1);c.push(k)}}return c}d||c.warn("pagination is now deprecated. Use uib-pagination instead.");var k=h[0],l=h[1];if(l){var m=angular.isDefined(g.maxSize)?e.$parent.$eval(g.maxSize):b.maxSize,n=angular.isDefined(g.rotate)?e.$parent.$eval(g.rotate):b.rotate;e.boundaryLinks=angular.isDefined(g.boundaryLinks)?e.$parent.$eval(g.boundaryLinks):b.boundaryLinks,e.directionLinks=angular.isDefined(g.directionLinks)?e.$parent.$eval(g.directionLinks):b.directionLinks,k.init(l,b),g.maxSize&&e.$parent.$watch(a(g.maxSize),function(a){m=parseInt(a,10),k.render()});var o=k.render;k.render=function(){o(),e.page>0&&e.page<=e.totalPages&&(e.pages=j(e.page,e.totalPages))}}}}}]).directive("pager",["uibPagerConfig","$log","$paginationSuppressWarning",function(a,b,c){return{restrict:"EA",scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["pager","?ngModel"],controller:"PaginationController",controllerAs:"pagination",templateUrl:function(a,b){return b.templateUrl||"template/pagination/pager.html"},replace:!0,link:function(d,e,f,g){c||b.warn("pager is now deprecated. Use uib-pager instead.");var h=g[0],i=g[1];i&&(d.align=angular.isDefined(f.align)?d.$parent.$eval(f.align):a.align,h.init(i,a))}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.stackedMap"]).provider("$uibTooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0,popupCloseDelay:500,useContentExp:!1},c={mouseenter:"mouseleave",click:"click",focus:"blur",none:""},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$document","$uibPosition","$interpolate","$rootScope","$parse","$$stackedMap",function(e,f,g,h,i,j,k,l,m){var n=m.createNew();return h.on("keypress",function(a){if(27===a.which){var b=n.top();b&&(b.value.close(),n.removeTop(),b=null)}}),function(e,k,m,o){function p(a){var b=(a||o.trigger||m).split(" "),d=b.map(function(a){return c[a]||a});return{show:b,hide:d}}o=angular.extend({},b,d,o);var q=a(e),r=j.startSymbol(),s=j.endSymbol(),t="<div "+q+'-popup title="'+r+"title"+s+'" '+(o.useContentExp?'content-exp="contentExp()" ':'content="'+r+"content"+s+'" ')+'placement="'+r+"placement"+s+'" popup-class="'+r+"popupClass"+s+'" animation="animation" is-open="isOpen"origin-scope="origScope" style="visibility: hidden; display: block;"></div>';return{compile:function(a,b){var c=f(t);return function(a,b,d,f){function j(){H.isOpen?q():m()}function m(){(!G||a.$eval(d[k+"Enable"]))&&(v(),H.popupDelay?C||(C=g(r,H.popupDelay,!1)):r())}function q(){s()}function r(){return C&&(g.cancel(C),C=null),B&&(g.cancel(B),B=null),H.content?(t(),void H.$evalAsync(function(){H.isOpen=!0,J&&angular.isFunction(J.assign)&&J.assign(H.origScope,H.isOpen),M()})):angular.noop}function s(){H&&(H.$evalAsync(function(){H.isOpen=!1,J&&angular.isFunction(J.assign)&&J.assign(H.origScope,H.isOpen)}),g.cancel(C),C=null,g.cancel(D),D=null,H.animation?B||(B=g(u,H.popupCloseDelay)):u())}function t(){z||(A=H.$new(),z=c(A,function(a){E?h.find("body").append(a):b.after(a)}),w())}function u(){x(),B=null,z&&(z.remove(),z=null),A&&(A.$destroy(),A=null)}function v(){H.title=d[k+"Title"],K?H.content=K(a):H.content=d[e],H.popupClass=d[k+"Class"],H.placement=angular.isDefined(d[k+"Placement"])?d[k+"Placement"]:o.placement;var b=parseInt(d[k+"PopupDelay"],10),c=parseInt(d[k+"PopupCloseDelay"],10);H.popupDelay=isNaN(b)?o.popupDelay:b,H.popupCloseDelay=isNaN(c)?o.popupCloseDelay:c}function w(){L.length=0,K?(L.push(a.$watch(K,function(a){H.content=a,!a&&H.isOpen&&s()})),L.push(A.$watch(function(){I||(I=!0,A.$$postDigest(function(){I=!1,H&&H.isOpen&&M()}))}))):L.push(d.$observe(e,function(a){H.content=a,!a&&H.isOpen?s():M()})),L.push(d.$observe(k+"Title",function(a){H.title=a,H.isOpen&&M()})),L.push(d.$observe(k+"Placement",function(a){H.placement=a?a:o.placement,H.isOpen&&M()}))}function x(){L.length&&(angular.forEach(L,function(a){a()}),L.length=0)}function y(){var a=d[k+"Trigger"];N(),F=p(a),"none"!==F.show&&F.show.forEach(function(a,c){a===F.hide[c]?b[0].addEventListener(a,j):a&&(b[0].addEventListener(a,m),F.hide[c].split(" ").forEach(function(a){b[0].addEventListener(a,q)})),b.on("keypress",function(a){27===a.which&&q()})})}var z,A,B,C,D,E=angular.isDefined(o.appendToBody)?o.appendToBody:!1,F=p(void 0),G=angular.isDefined(d[k+"Enable"]),H=a.$new(!0),I=!1,J=angular.isDefined(d[k+"IsOpen"])?l(d[k+"IsOpen"]):!1,K=o.useContentExp?l(d[e]):!1,L=[],M=function(){z&&z.html()&&(D||(D=g(function(){z.css({top:0,left:0});var a=i.positionElements(b,z,H.placement,E);a.top+="px",a.left+="px",a.visibility="visible",z.css(a),D=null},0,!1)))};H.origScope=a,H.isOpen=!1,n.add(H,{close:s}),H.contentExp=function(){return H.content},d.$observe("disabled",function(a){C&&a&&(g.cancel(C),C=null),a&&H.isOpen&&s()}),J&&a.$watch(J,function(a){!a===H.isOpen&&j()});var N=function(){F.show.forEach(function(a){b.unbind(a,m)}),F.hide.forEach(function(a){a.split(" ").forEach(function(a){b[0].removeEventListener(a,q)})})};y();var O=a.$eval(d[k+"Animation"]);H.animation=angular.isDefined(O)?!!O:o.animation;var P=a.$eval(d[k+"AppendToBody"]);E=angular.isDefined(P)?P:E,E&&a.$on("$locationChangeSuccess",function(){H.isOpen&&s()}),a.$on("$destroy",function(){g.cancel(B),g.cancel(C),g.cancel(D),N(),u(),H=null})}}}}}]}).directive("uibTooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest",function(a,b,c,d){return{link:function(e,f,g){var h,i,j,k=e.$eval(g.tooltipTemplateTranscludeScope),l=0,m=function(){i&&(i.remove(),i=null),h&&(h.$destroy(),h=null),j&&(a.leave(j).then(function(){i=null}),i=j,j=null)};e.$watch(b.parseAsResourceUrl(g.uibTooltipTemplateTransclude),function(b){var g=++l;b?(d(b,!0).then(function(d){if(g===l){var e=k.$new(),i=d,n=c(i)(e,function(b){m(),a.enter(b,f)});h=e,j=n,h.$emit("$includeContentLoaded",b)}},function(){g===l&&(m(),e.$emit("$includeContentError",b))}),e.$emit("$includeContentRequested",b)):m()}),e.$on("$destroy",m)}}}]).directive("uibTooltipClasses",function(){return{restrict:"A",link:function(a,b,c){a.placement&&b.addClass(a.placement),a.popupClass&&b.addClass(a.popupClass),a.animation()&&b.addClass(c.tooltipAnimationClass)}}}).directive("uibTooltipPopup",function(){return{replace:!0,scope:{content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html",link:function(a,b){b.addClass("tooltip")}}}).directive("uibTooltip",["$uibTooltip",function(a){return a("uibTooltip","tooltip","mouseenter")}]).directive("uibTooltipTemplatePopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"template/tooltip/tooltip-template-popup.html",link:function(a,b){b.addClass("tooltip")}}}).directive("uibTooltipTemplate",["$uibTooltip",function(a){return a("uibTooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("uibTooltipHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-popup.html",link:function(a,b){b.addClass("tooltip")}}}).directive("uibTooltipHtml",["$uibTooltip",function(a){return a("uibTooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]),angular.module("ui.bootstrap.tooltip").value("$tooltipSuppressWarning",!1).provider("$tooltip",["$uibTooltipProvider",function(a){angular.extend(this,a),this.$get=["$log","$tooltipSuppressWarning","$injector",function(b,c,d){return c||b.warn("$tooltip is now deprecated. Use $uibTooltip instead."),d.invoke(a.$get)}]}]).directive("tooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest","$log","$tooltipSuppressWarning",function(a,b,c,d,e,f){return{link:function(g,h,i){f||e.warn("tooltip-template-transclude is now deprecated. Use uib-tooltip-template-transclude instead.");var j,k,l,m=g.$eval(i.tooltipTemplateTranscludeScope),n=0,o=function(){k&&(k.remove(),k=null),j&&(j.$destroy(),j=null),l&&(a.leave(l).then(function(){k=null}),k=l,l=null)};g.$watch(b.parseAsResourceUrl(i.tooltipTemplateTransclude),function(b){var e=++n;b?(d(b,!0).then(function(d){if(e===n){var f=m.$new(),g=d,i=c(g)(f,function(b){o(),a.enter(b,h)});j=f,l=i,j.$emit("$includeContentLoaded",b)}},function(){e===n&&(o(),g.$emit("$includeContentError",b))}),g.$emit("$includeContentRequested",b)):o()}),g.$on("$destroy",o)}}}]).directive("tooltipClasses",["$log","$tooltipSuppressWarning",function(a,b){return{restrict:"A",link:function(c,d,e){b||a.warn("tooltip-classes is now deprecated. Use uib-tooltip-classes instead."),c.placement&&d.addClass(c.placement),c.popupClass&&d.addClass(c.popupClass),c.animation()&&d.addClass(e.tooltipAnimationClass)}}}]).directive("tooltipPopup",["$log","$tooltipSuppressWarning",function(a,b){return{replace:!0,scope:{content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html",link:function(c,d){b||a.warn("tooltip-popup is now deprecated. Use uib-tooltip-popup instead."),d.addClass("tooltip")}}}]).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipTemplatePopup",["$log","$tooltipSuppressWarning",function(a,b){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"template/tooltip/tooltip-template-popup.html",link:function(c,d){b||a.warn("tooltip-template-popup is now deprecated. Use uib-tooltip-template-popup instead."),d.addClass("tooltip")}}}]).directive("tooltipTemplate",["$tooltip",function(a){return a("tooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("tooltipHtmlPopup",["$log","$tooltipSuppressWarning",function(a,b){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-popup.html",link:function(c,d){b||a.warn("tooltip-html-popup is now deprecated. Use uib-tooltip-html-popup instead."),d.addClass("tooltip")}}}]).directive("tooltipHtml",["$tooltip",function(a){return a("tooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("uibPopoverTemplatePopup",function(){return{replace:!0,scope:{title:"@",contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"template/popover/popover-template.html",link:function(a,b){b.addClass("popover")}}}).directive("uibPopoverTemplate",["$uibTooltip",function(a){return a("uibPopoverTemplate","popover","click",{useContentExp:!0})}]).directive("uibPopoverHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",title:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover-html.html",link:function(a,b){b.addClass("popover")}}}).directive("uibPopoverHtml",["$uibTooltip",function(a){return a("uibPopoverHtml","popover","click",{useContentExp:!0})}]).directive("uibPopoverPopup",function(){return{replace:!0,scope:{title:"@",content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html",link:function(a,b){b.addClass("popover")}}}).directive("uibPopover",["$uibTooltip",function(a){return a("uibPopover","popover","click")}]),angular.module("ui.bootstrap.popover").value("$popoverSuppressWarning",!1).directive("popoverTemplatePopup",["$log","$popoverSuppressWarning",function(a,b){return{replace:!0,scope:{title:"@",contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"template/popover/popover-template.html",link:function(c,d){b||a.warn("popover-template-popup is now deprecated. Use uib-popover-template-popup instead."),d.addClass("popover")}}}]).directive("popoverTemplate",["$tooltip",function(a){return a("popoverTemplate","popover","click",{useContentExp:!0})}]).directive("popoverHtmlPopup",["$log","$popoverSuppressWarning",function(a,b){return{replace:!0,scope:{contentExp:"&",title:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover-html.html",link:function(c,d){b||a.warn("popover-html-popup is now deprecated. Use uib-popover-html-popup instead."),d.addClass("popover")}}}]).directive("popoverHtml",["$tooltip",function(a){return a("popoverHtml","popover","click",{useContentExp:!0})}]).directive("popoverPopup",["$log","$popoverSuppressWarning",function(a,b){return{replace:!0,scope:{title:"@",content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html",link:function(c,d){b||a.warn("popover-popup is now deprecated. Use uib-popover-popup instead."),d.addClass("popover")}}}]).directive("popover",["$tooltip",function(a){return a("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("uibProgressConfig",{animate:!0,max:100}).controller("UibProgressController",["$scope","$attrs","uibProgressConfig",function(a,b,c){var d=this,e=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(a.max)?a.max:c.max,this.addBar=function(b,c,f){e||c.css({transition:"none"}),this.bars.push(b),b.max=a.max,b.title=f&&angular.isDefined(f.title)?f.title:"progressbar",b.$watch("value",function(a){b.recalculatePercentage()}),b.recalculatePercentage=function(){b.percent=+(100*b.value/b.max).toFixed(2);var a=d.bars.reduce(function(a,b){return a+b.percent},0);a>100&&(b.percent-=a-100)},b.$on("$destroy",function(){c=null,d.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)},a.$watch("max",function(b){d.bars.forEach(function(b){b.max=a.max,b.recalculatePercentage()})})}]).directive("uibProgress",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",require:"uibProgress",scope:{max:"=?"},templateUrl:"template/progressbar/progress.html"}}).directive("uibBar",function(){return{replace:!0,transclude:!0,require:"^uibProgress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b,c)}}}).directive("uibProgressbar",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",scope:{value:"=",max:"=?",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]),{title:c.title})}}}),angular.module("ui.bootstrap.progressbar").value("$progressSuppressWarning",!1).controller("ProgressController",["$scope","$attrs","uibProgressConfig","$log","$progressSuppressWarning",function(a,b,c,d,e){e||d.warn("ProgressController is now deprecated. Use UibProgressController instead.");var f=this,g=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=angular.isDefined(a.max)?a.max:c.max,this.addBar=function(b,c,d){g||c.css({transition:"none"}),this.bars.push(b),b.max=a.max,b.title=d&&angular.isDefined(d.title)?d.title:"progressbar",b.$watch("value",function(a){b.recalculatePercentage()}),b.recalculatePercentage=function(){b.percent=+(100*b.value/b.max).toFixed(2);var a=f.bars.reduce(function(a,b){return a+b.percent},0);a>100&&(b.percent-=a-100)},b.$on("$destroy",function(){c=null,f.removeBar(b)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1)},a.$watch("max",function(b){f.bars.forEach(function(b){b.max=a.max,b.recalculatePercentage()})})}]).directive("progress",["$log","$progressSuppressWarning",function(a,b){return{replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{max:"=?",title:"@?"},templateUrl:"template/progressbar/progress.html",link:function(){b||a.warn("progress is now deprecated. Use uib-progress instead.")}}}]).directive("bar",["$log","$progressSuppressWarning",function(a,b){return{replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(c,d,e,f){b||a.warn("bar is now deprecated. Use uib-bar instead."),f.addBar(c,d)}}}]).directive("progressbar",["$log","$progressSuppressWarning",function(a,b){return{replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",max:"=?",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(c,d,e,f){b||a.warn("progressbar is now deprecated. Use uib-progressbar instead."),f.addBar(c,angular.element(d.children()[0]),{title:e.title})}}}]),angular.module("ui.bootstrap.rating",[]).constant("uibRatingConfig",{max:5,stateOn:null,stateOff:null,titles:["one","two","three","four","five"]}).controller("UibRatingController",["$scope","$attrs","uibRatingConfig",function(a,b,c){var d={$setViewValue:angular.noop};this.init=function(e){d=e,d.$render=this.render,d.$formatters.push(function(a){return angular.isNumber(a)&&a<<0!==a&&(a=Math.round(a)),a}),this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.titles)?a.$parent.$eval(b.titles):c.titles;this.titles=angular.isArray(f)&&f.length>0?f:c.titles;var g=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(g)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff,title:this.getTitle(b)},a[b]);return a},this.getTitle=function(a){return a>=this.titles.length?a+1:this.titles[a]},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(d.$viewValue===b?0:b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue}}]).directive("uibRating",function(){return{require:["uibRating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"UibRatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}),angular.module("ui.bootstrap.rating").value("$ratingSuppressWarning",!1).controller("RatingController",["$scope","$attrs","$controller","$log","$ratingSuppressWarning",function(a,b,c,d,e){e||d.warn("RatingController is now deprecated. Use UibRatingController instead."),angular.extend(this,c("UibRatingController",{$scope:a,$attrs:b}))}]).directive("rating",["$log","$ratingSuppressWarning",function(a,b){return{require:["rating","ngModel"],scope:{readonly:"=?",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0,link:function(c,d,e,f){b||a.warn("rating is now deprecated. Use uib-rating instead.");var g=f[0],h=f[1];g.init(h)}}}]),angular.module("ui.bootstrap.tabs",[]).controller("UibTabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[];b.select=function(a){angular.forEach(c,function(b){b.active&&b!==a&&(b.active=!1,b.onDeselect(),a.selectCalled=!1)}),a.active=!0,a.selectCalled||(a.onSelect(),a.selectCalled=!0)},b.addTab=function(a){c.push(a),1===c.length&&a.active!==!1?a.active=!0:a.active?b.select(a):a.active=!1},b.removeTab=function(a){var e=c.indexOf(a);if(a.active&&c.length>1&&!d){var f=e==c.length-1?e-1:e+1;b.select(c[f])}c.splice(e,1)};var d;a.$on("$destroy",function(){d=!0})}]).directive("uibTabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"UibTabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1}}}).directive("uibTab",["$parse",function(a){return{require:"^uibTabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},link:function(b,c,d,e,f){b.$watch("active",function(a){a&&e.select(b)}),b.disabled=!1,d.disable&&b.$parent.$watch(a(d.disable),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},e.addTab(b),b.$on("$destroy",function(){e.removeTab(b)}),b.$transcludeFn=f}}}]).directive("uibTabHeadingTransclude",function(){return{restrict:"A",require:["?^uibTab","?^tab"],link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}).directive("uibTabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||a.hasAttribute("x-tab-heading")||a.hasAttribute("uib-tab-heading")||a.hasAttribute("data-uib-tab-heading")||a.hasAttribute("x-uib-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase()||"x-tab-heading"===a.tagName.toLowerCase()||"uib-tab-heading"===a.tagName.toLowerCase()||"data-uib-tab-heading"===a.tagName.toLowerCase()||"x-uib-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:["?^uibTabset","?^tabset"],link:function(b,c,d){var e=b.$eval(d.uibTabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.tabs").value("$tabsSuppressWarning",!1).controller("TabsetController",["$scope","$controller","$log","$tabsSuppressWarning",function(a,b,c,d){d||c.warn("TabsetController is now deprecated. Use UibTabsetController instead."),angular.extend(this,b("UibTabsetController",{$scope:a}))}]).directive("tabset",["$log","$tabsSuppressWarning",function(a,b){return{restrict:"EA",transclude:!0,replace:!0,scope:{type:"@"},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(c,d,e){b||a.warn("tabset is now deprecated. Use uib-tabset instead."),c.vertical=angular.isDefined(e.vertical)?c.$parent.$eval(e.vertical):!1,c.justified=angular.isDefined(e.justified)?c.$parent.$eval(e.justified):!1}}}]).directive("tab",["$parse","$log","$tabsSuppressWarning",function(a,b,c){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{active:"=?",heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},link:function(d,e,f,g,h){c||b.warn("tab is now deprecated. Use uib-tab instead."),d.$watch("active",function(a){a&&g.select(d)}),d.disabled=!1,f.disable&&d.$parent.$watch(a(f.disable),function(a){d.disabled=!!a}),d.select=function(){d.disabled||(d.active=!0)},g.addTab(d),d.$on("$destroy",function(){g.removeTab(d)}),d.$transcludeFn=h}}}]).directive("tabHeadingTransclude",["$log","$tabsSuppressWarning",function(a,b){return{restrict:"A",require:"^tab",link:function(c,d){b||a.warn("tab-heading-transclude is now deprecated. Use uib-tab-heading-transclude instead."),c.$watch("headingElement",function(a){a&&(d.html(""),d.append(a))})}}}]).directive("tabContentTransclude",["$log","$tabsSuppressWarning",function(a,b){function c(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||a.hasAttribute("x-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase()||"x-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(d,e,f){b||a.warn("tab-content-transclude is now deprecated. Use uib-tab-content-transclude instead.");var g=d.$eval(f.tabContentTransclude);g.$transcludeFn(g.$parent,function(a){angular.forEach(a,function(a){c(a)?g.headingElement=a:e.append(a)})})}}}]),angular.module("ui.bootstrap.timepicker",[]).constant("uibTimepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0,arrowkeys:!0,showSpinners:!0}).controller("UibTimepickerController",["$scope","$element","$attrs","$parse","$log","$locale","uibTimepickerConfig",function(a,b,c,d,e,f,g){function h(){var b=parseInt(a.hours,10),c=a.showMeridian?b>0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===r[1]&&(b+=12)),b):void 0}function i(){var b=parseInt(a.minutes,10);return b>=0&&60>b?b:void 0}function j(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a.toString()}function k(a){l(),q.$setViewValue(new Date(p)),m(a)}function l(){q.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1}function m(b){var c=p.getHours(),d=p.getMinutes();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:j(c),"m"!==b&&(a.minutes=j(d)),a.meridian=p.getHours()<12?r[0]:r[1]}function n(a,b){var c=new Date(a.getTime()+6e4*b),d=new Date(a);return d.setHours(c.getHours(),c.getMinutes()),d}function o(a){p=n(p,a),k()}var p=new Date,q={$setViewValue:angular.noop},r=angular.isDefined(c.meridians)?a.$parent.$eval(c.meridians):g.meridians||f.DATETIME_FORMATS.AMPMS;a.tabindex=angular.isDefined(c.tabindex)?c.tabindex:0,b.removeAttr("tabindex"),this.init=function(b,d){q=b,q.$render=this.render,q.$formatters.unshift(function(a){return a?new Date(a):null});var e=d.eq(0),f=d.eq(1),h=angular.isDefined(c.mousewheel)?a.$parent.$eval(c.mousewheel):g.mousewheel;h&&this.setupMousewheelEvents(e,f);var i=angular.isDefined(c.arrowkeys)?a.$parent.$eval(c.arrowkeys):g.arrowkeys;i&&this.setupArrowkeyEvents(e,f),a.readonlyInput=angular.isDefined(c.readonlyInput)?a.$parent.$eval(c.readonlyInput):g.readonlyInput,this.setupInputEvents(e,f)};var s=g.hourStep;c.hourStep&&a.$parent.$watch(d(c.hourStep),function(a){s=parseInt(a,10)});var t=g.minuteStep;c.minuteStep&&a.$parent.$watch(d(c.minuteStep),function(a){t=parseInt(a,10)});var u;a.$parent.$watch(d(c.min),function(a){var b=new Date(a);u=isNaN(b)?void 0:b});var v;a.$parent.$watch(d(c.max),function(a){var b=new Date(a);v=isNaN(b)?void 0:b}),a.noIncrementHours=function(){var a=n(p,60*s);return a>v||p>a&&u>a},a.noDecrementHours=function(){var a=n(p,60*-s);return u>a||a>p&&a>v},a.noIncrementMinutes=function(){var a=n(p,t);return a>v||p>a&&u>a},a.noDecrementMinutes=function(){var a=n(p,-t);return u>a||a>p&&a>v},a.noToggleMeridian=function(){return p.getHours()<13?n(p,720)>v:n(p,-720)<u},a.showMeridian=g.showMeridian,c.showMeridian&&a.$parent.$watch(d(c.showMeridian),function(b){if(a.showMeridian=!!b,q.$error.time){
10 var c=h(),d=i();angular.isDefined(c)&&angular.isDefined(d)&&(p.setHours(c),k())}else m()}),this.setupMousewheelEvents=function(b,c){var d=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};b.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){a.$apply(d(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()})},this.setupArrowkeyEvents=function(b,c){b.bind("keydown",function(b){38===b.which?(b.preventDefault(),a.incrementHours(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementHours(),a.$apply())}),c.bind("keydown",function(b){38===b.which?(b.preventDefault(),a.incrementMinutes(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementMinutes(),a.$apply())})},this.setupInputEvents=function(b,c){if(a.readonlyInput)return a.updateHours=angular.noop,void(a.updateMinutes=angular.noop);var d=function(b,c){q.$setViewValue(null),q.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c)};a.updateHours=function(){var a=h(),b=i();angular.isDefined(a)&&angular.isDefined(b)?(p.setHours(a),u>p||p>v?d(!0):k("h")):d(!0)},b.bind("blur",function(b){!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=j(a.hours)})}),a.updateMinutes=function(){var a=i(),b=h();angular.isDefined(a)&&angular.isDefined(b)?(p.setMinutes(a),u>p||p>v?d(void 0,!0):k("m")):d(void 0,!0)},c.bind("blur",function(b){!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=j(a.minutes)})})},this.render=function(){var b=q.$viewValue;isNaN(b)?(q.$setValidity("time",!1),e.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(b&&(p=b),u>p||p>v?(q.$setValidity("time",!1),a.invalidHours=!0,a.invalidMinutes=!0):l(),m())},a.showSpinners=angular.isDefined(c.showSpinners)?a.$parent.$eval(c.showSpinners):g.showSpinners,a.incrementHours=function(){a.noIncrementHours()||o(60*s)},a.decrementHours=function(){a.noDecrementHours()||o(60*-s)},a.incrementMinutes=function(){a.noIncrementMinutes()||o(t)},a.decrementMinutes=function(){a.noDecrementMinutes()||o(-t)},a.toggleMeridian=function(){a.noToggleMeridian()||o(720*(p.getHours()<12?1:-1))}}]).directive("uibTimepicker",function(){return{restrict:"EA",require:["uibTimepicker","?^ngModel"],controller:"UibTimepickerController",controllerAs:"timepicker",replace:!0,scope:{},templateUrl:function(a,b){return b.templateUrl||"template/timepicker/timepicker.html"},link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}),angular.module("ui.bootstrap.timepicker").value("$timepickerSuppressWarning",!1).controller("TimepickerController",["$scope","$element","$attrs","$controller","$log","$timepickerSuppressWarning",function(a,b,c,d,e,f){f||e.warn("TimepickerController is now deprecated. Use UibTimepickerController instead."),angular.extend(this,d("UibTimepickerController",{$scope:a,$element:b,$attrs:c}))}]).directive("timepicker",["$log","$timepickerSuppressWarning",function(a,b){return{restrict:"EA",require:["timepicker","?^ngModel"],controller:"TimepickerController",controllerAs:"timepicker",replace:!0,scope:{},templateUrl:function(a,b){return b.templateUrl||"template/timepicker/timepicker.html"},link:function(c,d,e,f){b||a.warn("timepicker is now deprecated. Use uib-timepicker instead.");var g=f[0],h=f[1];h&&g.init(h,d.find("input"))}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position"]).factory("uibTypeaheadParser",["$parse",function(a){var b=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("uibTypeahead",["$compile","$parse","$q","$timeout","$document","$window","$rootScope","$uibPosition","uibTypeaheadParser",function(a,b,c,d,e,f,g,h,i){var j=[9,13,27,38,40],k=200;return{require:["ngModel","^?ngModelOptions"],link:function(l,m,n,o){function p(){L.moveInProgress||(L.moveInProgress=!0,L.$digest()),T&&d.cancel(T),T=d(function(){L.matches.length&&q(),L.moveInProgress=!1},k)}function q(){L.position=D?h.offset(m):h.position(m),L.position.top+=m.prop("offsetHeight")}var r=o[0],s=o[1],t=l.$eval(n.typeaheadMinLength);t||0===t||(t=1);var u,v,w=l.$eval(n.typeaheadWaitMs)||0,x=l.$eval(n.typeaheadEditable)!==!1,y=b(n.typeaheadLoading).assign||angular.noop,z=b(n.typeaheadOnSelect),A=angular.isDefined(n.typeaheadSelectOnBlur)?l.$eval(n.typeaheadSelectOnBlur):!1,B=b(n.typeaheadNoResults).assign||angular.noop,C=n.typeaheadInputFormatter?b(n.typeaheadInputFormatter):void 0,D=n.typeaheadAppendToBody?l.$eval(n.typeaheadAppendToBody):!1,E=n.typeaheadAppendToElementId||!1,F=l.$eval(n.typeaheadFocusFirst)!==!1,G=n.typeaheadSelectOnExact?l.$eval(n.typeaheadSelectOnExact):!1,H=b(n.ngModel),I=b(n.ngModel+"($$$p)"),J=function(a,b){return angular.isFunction(H(l))&&s&&s.$options&&s.$options.getterSetter?I(a,{$$$p:b}):H.assign(a,b)},K=i.parse(n.uibTypeahead),L=l.$new(),M=l.$on("$destroy",function(){L.$destroy()});L.$on("$destroy",M);var N="typeahead-"+L.$id+"-"+Math.floor(1e4*Math.random());m.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":N});var O=angular.element("<div uib-typeahead-popup></div>");O.attr({id:N,matches:"matches",active:"activeIdx",select:"select(activeIdx)","move-in-progress":"moveInProgress",query:"query",position:"position"}),angular.isDefined(n.typeaheadTemplateUrl)&&O.attr("template-url",n.typeaheadTemplateUrl),angular.isDefined(n.typeaheadPopupTemplateUrl)&&O.attr("popup-template-url",n.typeaheadPopupTemplateUrl);var P=function(){L.matches=[],L.activeIdx=-1,m.attr("aria-expanded",!1)},Q=function(a){return N+"-option-"+a};L.$watch("activeIdx",function(a){0>a?m.removeAttr("aria-activedescendant"):m.attr("aria-activedescendant",Q(a))});var R=function(a,b){return L.matches.length>b&&a?a.toUpperCase()===L.matches[b].label.toUpperCase():!1},S=function(a){var b={$viewValue:a};y(l,!0),B(l,!1),c.when(K.source(l,b)).then(function(c){var d=a===r.$viewValue;if(d&&u)if(c&&c.length>0){L.activeIdx=F?0:-1,B(l,!1),L.matches.length=0;for(var e=0;e<c.length;e++)b[K.itemName]=c[e],L.matches.push({id:Q(e),label:K.viewMapper(L,b),model:c[e]});L.query=a,q(),m.attr("aria-expanded",!0),G&&1===L.matches.length&&R(a,0)&&L.select(0)}else P(),B(l,!0);d&&y(l,!1)},function(){P(),y(l,!1),B(l,!0)})};D&&(angular.element(f).bind("resize",p),e.find("body").bind("scroll",p));var T;L.moveInProgress=!1,P(),L.query=void 0;var U,V=function(a){U=d(function(){S(a)},w)},W=function(){U&&d.cancel(U)};r.$parsers.unshift(function(a){return u=!0,0===t||a&&a.length>=t?w>0?(W(),V(a)):S(a):(y(l,!1),W(),P()),x?a:a?void r.$setValidity("editable",!1):(r.$setValidity("editable",!0),null)}),r.$formatters.push(function(a){var b,c,d={};return x||r.$setValidity("editable",!0),C?(d.$model=a,C(l,d)):(d[K.itemName]=a,b=K.viewMapper(l,d),d[K.itemName]=void 0,c=K.viewMapper(l,d),b!==c?b:a)}),L.select=function(a){var b,c,e={};v=!0,e[K.itemName]=c=L.matches[a].model,b=K.modelMapper(l,e),J(l,b),r.$setValidity("editable",!0),r.$setValidity("parse",!0),z(l,{$item:c,$model:b,$label:K.viewMapper(l,e)}),P(),L.$eval(n.typeaheadFocusOnSelect)!==!1&&d(function(){m[0].focus()},0,!1)},m.bind("keydown",function(a){if(0!==L.matches.length&&-1!==j.indexOf(a.which)){if(-1===L.activeIdx&&(9===a.which||13===a.which))return P(),void L.$digest();a.preventDefault(),40===a.which?(L.activeIdx=(L.activeIdx+1)%L.matches.length,L.$digest()):38===a.which?(L.activeIdx=(L.activeIdx>0?L.activeIdx:L.matches.length)-1,L.$digest()):13===a.which||9===a.which?L.$apply(function(){L.select(L.activeIdx)}):27===a.which&&(a.stopPropagation(),P(),L.$digest())}}),m.bind("blur",function(){A&&L.matches.length&&-1!==L.activeIdx&&!v&&(v=!0,L.$apply(function(){L.select(L.activeIdx)})),u=!1,v=!1});var X=function(a){m[0]!==a.target&&3!==a.which&&0!==L.matches.length&&(P(),g.$$phase||L.$digest())};e.bind("click",X),l.$on("$destroy",function(){e.unbind("click",X),(D||E)&&Y.remove(),O.remove()});var Y=a(O)(L);D?e.find("body").append(Y):E!==!1?angular.element(e[0].getElementById(E)).append(Y):m.after(Y)}}}]).directive("uibTypeaheadPopup",function(){return{scope:{matches:"=",query:"=",active:"=",position:"&",moveInProgress:"=",select:"&"},replace:!0,templateUrl:function(a,b){return b.popupTemplateUrl||"template/typeahead/typeahead-popup.html"},link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("uibTypeaheadMatch",["$templateRequest","$compile","$parse",function(a,b,c){return{scope:{index:"=",match:"=",query:"="},link:function(d,e,f){var g=c(f.templateUrl)(d.$parent)||"template/typeahead/typeahead-match.html";a(g).then(function(a){b(a.trim())(d,function(a){e.replaceWith(a)})})}}}]).filter("uibTypeaheadHighlight",["$sce","$injector","$log",function(a,b,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}function e(a){return/<.*>/g.test(a)}var f;return f=b.has("$sanitize"),function(b,g){return!f&&e(b)&&c.warn("Unsafe use of typeahead please use ngSanitize"),b=g?(""+b).replace(new RegExp(d(g),"gi"),"<strong>$&</strong>"):b,f||(b=a.trustAsHtml(b)),b}}]),angular.module("ui.bootstrap.typeahead").value("$typeaheadSuppressWarning",!1).service("typeaheadParser",["$parse","uibTypeaheadParser","$log","$typeaheadSuppressWarning",function(a,b,c,d){return d||c.warn("typeaheadParser is now deprecated. Use uibTypeaheadParser instead."),b}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$window","$rootScope","$uibPosition","typeaheadParser","$log","$typeaheadSuppressWarning",function(a,b,c,d,e,f,g,h,i,j,k){var l=[9,13,27,38,40],m=200;return{require:["ngModel","^?ngModelOptions"],link:function(n,o,p,q){function r(){N.moveInProgress||(N.moveInProgress=!0,N.$digest()),V&&d.cancel(V),V=d(function(){N.matches.length&&s(),N.moveInProgress=!1},m)}function s(){N.position=F?h.offset(o):h.position(o),N.position.top+=o.prop("offsetHeight")}k||j.warn("typeahead is now deprecated. Use uib-typeahead instead.");var t=q[0],u=q[1],v=n.$eval(p.typeaheadMinLength);v||0===v||(v=1);var w,x,y=n.$eval(p.typeaheadWaitMs)||0,z=n.$eval(p.typeaheadEditable)!==!1,A=b(p.typeaheadLoading).assign||angular.noop,B=b(p.typeaheadOnSelect),C=angular.isDefined(p.typeaheadSelectOnBlur)?n.$eval(p.typeaheadSelectOnBlur):!1,D=b(p.typeaheadNoResults).assign||angular.noop,E=p.typeaheadInputFormatter?b(p.typeaheadInputFormatter):void 0,F=p.typeaheadAppendToBody?n.$eval(p.typeaheadAppendToBody):!1,G=p.typeaheadAppendToElementId||!1,H=n.$eval(p.typeaheadFocusFirst)!==!1,I=p.typeaheadSelectOnExact?n.$eval(p.typeaheadSelectOnExact):!1,J=b(p.ngModel),K=b(p.ngModel+"($$$p)"),L=function(a,b){return angular.isFunction(J(n))&&u&&u.$options&&u.$options.getterSetter?K(a,{$$$p:b}):J.assign(a,b)},M=i.parse(p.typeahead),N=n.$new(),O=n.$on("$destroy",function(){N.$destroy()});N.$on("$destroy",O);var P="typeahead-"+N.$id+"-"+Math.floor(1e4*Math.random());o.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":P});var Q=angular.element("<div typeahead-popup></div>");Q.attr({id:P,matches:"matches",active:"activeIdx",select:"select(activeIdx)","move-in-progress":"moveInProgress",query:"query",position:"position"}),angular.isDefined(p.typeaheadTemplateUrl)&&Q.attr("template-url",p.typeaheadTemplateUrl),angular.isDefined(p.typeaheadPopupTemplateUrl)&&Q.attr("popup-template-url",p.typeaheadPopupTemplateUrl);var R=function(){N.matches=[],N.activeIdx=-1,o.attr("aria-expanded",!1)},S=function(a){return P+"-option-"+a};N.$watch("activeIdx",function(a){0>a?o.removeAttr("aria-activedescendant"):o.attr("aria-activedescendant",S(a))});var T=function(a,b){return N.matches.length>b&&a?a.toUpperCase()===N.matches[b].label.toUpperCase():!1},U=function(a){var b={$viewValue:a};A(n,!0),D(n,!1),c.when(M.source(n,b)).then(function(c){var d=a===t.$viewValue;if(d&&w)if(c&&c.length>0){N.activeIdx=H?0:-1,D(n,!1),N.matches.length=0;for(var e=0;e<c.length;e++)b[M.itemName]=c[e],N.matches.push({id:S(e),label:M.viewMapper(N,b),model:c[e]});N.query=a,s(),o.attr("aria-expanded",!0),I&&1===N.matches.length&&T(a,0)&&N.select(0)}else R(),D(n,!0);d&&A(n,!1)},function(){R(),A(n,!1),D(n,!0)})};F&&(angular.element(f).bind("resize",r),e.find("body").bind("scroll",r));var V;N.moveInProgress=!1,R(),N.query=void 0;var W,X=function(a){W=d(function(){U(a)},y)},Y=function(){W&&d.cancel(W)};t.$parsers.unshift(function(a){return w=!0,0===v||a&&a.length>=v?y>0?(Y(),X(a)):U(a):(A(n,!1),Y(),R()),z?a:a?void t.$setValidity("editable",!1):(t.$setValidity("editable",!0),null)}),t.$formatters.push(function(a){var b,c,d={};return z||t.$setValidity("editable",!0),E?(d.$model=a,E(n,d)):(d[M.itemName]=a,b=M.viewMapper(n,d),d[M.itemName]=void 0,c=M.viewMapper(n,d),b!==c?b:a)}),N.select=function(a){var b,c,e={};x=!0,e[M.itemName]=c=N.matches[a].model,b=M.modelMapper(n,e),L(n,b),t.$setValidity("editable",!0),t.$setValidity("parse",!0),B(n,{$item:c,$model:b,$label:M.viewMapper(n,e)}),R(),N.$eval(p.typeaheadFocusOnSelect)!==!1&&d(function(){o[0].focus()},0,!1)},o.bind("keydown",function(a){if(0!==N.matches.length&&-1!==l.indexOf(a.which)){if(-1===N.activeIdx&&(9===a.which||13===a.which))return R(),void N.$digest();a.preventDefault(),40===a.which?(N.activeIdx=(N.activeIdx+1)%N.matches.length,N.$digest()):38===a.which?(N.activeIdx=(N.activeIdx>0?N.activeIdx:N.matches.length)-1,N.$digest()):13===a.which||9===a.which?N.$apply(function(){N.select(N.activeIdx)}):27===a.which&&(a.stopPropagation(),R(),N.$digest())}}),o.bind("blur",function(){C&&N.matches.length&&-1!==N.activeIdx&&!x&&(x=!0,N.$apply(function(){N.select(N.activeIdx)})),w=!1,x=!1});var Z=function(a){o[0]!==a.target&&3!==a.which&&0!==N.matches.length&&(R(),g.$$phase||N.$digest())};e.bind("click",Z),n.$on("$destroy",function(){e.unbind("click",Z),(F||G)&&$.remove(),Q.remove()});var $=a(Q)(N);F?e.find("body").append($):G!==!1?angular.element(e[0].getElementById(G)).append($):o.after($)}}}]).directive("typeaheadPopup",["$typeaheadSuppressWarning","$log",function(a,b){return{scope:{matches:"=",query:"=",active:"=",position:"&",moveInProgress:"=",select:"&"},replace:!0,templateUrl:function(a,b){return b.popupTemplateUrl||"template/typeahead/typeahead-popup.html"},link:function(c,d,e){a||b.warn("typeahead-popup is now deprecated. Use uib-typeahead-popup instead."),c.templateUrl=e.templateUrl,c.isOpen=function(){return c.matches.length>0},c.isActive=function(a){return c.active==a},c.selectActive=function(a){c.active=a},c.selectMatch=function(a){c.select({activeIdx:a})}}}}]).directive("typeaheadMatch",["$templateRequest","$compile","$parse","$typeaheadSuppressWarning","$log",function(a,b,c,d,e){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(f,g,h){d||e.warn("typeahead-match is now deprecated. Use uib-typeahead-match instead.");var i=c(h.templateUrl)(f.$parent)||"template/typeahead/typeahead-match.html";a(i).then(function(a){b(a.trim())(f,function(a){g.replaceWith(a)})})}}}]).filter("typeaheadHighlight",["$sce","$injector","$log","$typeaheadSuppressWarning",function(a,b,c,d){function e(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}function f(a){return/<.*>/g.test(a)}var g;return g=b.has("$sanitize"),function(b,h){return d||c.warn("typeaheadHighlight is now deprecated. Use uibTypeaheadHighlight instead."),!g&&f(b)&&c.warn("Unsafe use of typeahead please use ngSanitize"),b=h?(""+b).replace(new RegExp(e(h),"gi"),"<strong>$&</strong>"):b,g||(b=a.trustAsHtml(b)),b}}]),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'<div class="panel {{panelClass || \'panel-default\'}}">\n <div class="panel-heading" ng-keypress="toggleOpen($event)">\n <h4 class="panel-title">\n <a href tabindex="0" class="accordion-toggle" ng-click="toggleOpen()" uib-accordion-transclude="heading"><span ng-class="{\'text-muted\': isDisabled}">{{heading}}</span></a>\n </h4>\n </div>\n <div class="panel-collapse collapse" uib-collapse="!isOpen">\n <div class="panel-body" ng-transclude></div>\n </div>\n</div>\n')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'<div class="panel-group" ng-transclude></div>')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html",'<div class="alert" ng-class="[\'alert-\' + (type || \'warning\'), closeable ? \'alert-dismissible\' : null]" role="alert">\n <button ng-show="closeable" type="button" class="close" ng-click="close({$event: $event})">\n <span aria-hidden="true">&times;</span>\n <span class="sr-only">Close</span>\n </button>\n <div ng-transclude></div>\n</div>\n')}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'<div ng-mouseenter="pause()" ng-mouseleave="play()" class="carousel" ng-swipe-right="prev()" ng-swipe-left="next()">\n <div class="carousel-inner" ng-transclude></div>\n <a role="button" href class="left carousel-control" ng-click="prev()" ng-show="slides.length > 1">\n <span aria-hidden="true" class="glyphicon glyphicon-chevron-left"></span>\n <span class="sr-only">previous</span>\n </a>\n <a role="button" href class="right carousel-control" ng-click="next()" ng-show="slides.length > 1">\n <span aria-hidden="true" class="glyphicon glyphicon-chevron-right"></span>\n <span class="sr-only">next</span>\n </a>\n <ol class="carousel-indicators" ng-show="slides.length > 1">\n <li ng-repeat="slide in slides | orderBy:indexOfSlide track by $index" ng-class="{ active: isActive(slide) }" ng-click="select(slide)">\n <span class="sr-only">slide {{ $index + 1 }} of {{ slides.length }}<span ng-if="isActive(slide)">, currently active</span></span>\n </li>\n </ol>\n</div>')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html",'<div ng-class="{\n \'active\': active\n }" class="item text-center" ng-transclude></div>\n')}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'<div ng-switch="datepickerMode" role="application" ng-keydown="keydown($event)">\n <uib-daypicker ng-switch-when="day" tabindex="0"></uib-daypicker>\n <uib-monthpicker ng-switch-when="month" tabindex="0"></uib-monthpicker>\n <uib-yearpicker ng-switch-when="year" tabindex="0"></uib-yearpicker>\n</div>')}]),angular.module("template/datepicker/day.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/day.html",'<table role="grid" aria-labelledby="{{::uniqueId}}-title" aria-activedescendant="{{activeDateId}}">\n <thead>\n <tr>\n <th><button type="button" class="btn btn-default btn-sm pull-left" ng-click="move(-1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-left"></i></button></th>\n <th colspan="{{::5 + showWeeks}}"><button id="{{::uniqueId}}-title" role="heading" aria-live="assertive" aria-atomic="true" type="button" class="btn btn-default btn-sm" ng-click="toggleMode()" ng-disabled="datepickerMode === maxMode" tabindex="-1" style="width:100%;"><strong>{{title}}</strong></button></th>\n <th><button type="button" class="btn btn-default btn-sm pull-right" ng-click="move(1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-right"></i></button></th>\n </tr>\n <tr>\n <th ng-if="showWeeks" class="text-center"></th>\n <th ng-repeat="label in ::labels track by $index" class="text-center"><small aria-label="{{::label.full}}">{{::label.abbr}}</small></th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat="row in rows track by $index">\n <td ng-if="showWeeks" class="text-center h6"><em>{{ weekNumbers[$index] }}</em></td>\n <td ng-repeat="dt in row track by dt.date" class="text-center" role="gridcell" id="{{::dt.uid}}" ng-class="::dt.customClass">\n <button type="button" style="min-width:100%;" class="btn btn-default btn-sm" ng-class="{\'btn-info\': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="::{\'text-muted\': dt.secondary, \'text-info\': dt.current}">{{::dt.label}}</span></button>\n </td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/month.html",'<table role="grid" aria-labelledby="{{::uniqueId}}-title" aria-activedescendant="{{activeDateId}}">\n <thead>\n <tr>\n <th><button type="button" class="btn btn-default btn-sm pull-left" ng-click="move(-1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-left"></i></button></th>\n <th><button id="{{::uniqueId}}-title" role="heading" aria-live="assertive" aria-atomic="true" type="button" class="btn btn-default btn-sm" ng-click="toggleMode()" ng-disabled="datepickerMode === maxMode" tabindex="-1" style="width:100%;"><strong>{{title}}</strong></button></th>\n <th><button type="button" class="btn btn-default btn-sm pull-right" ng-click="move(1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-right"></i></button></th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat="row in rows track by $index">\n <td ng-repeat="dt in row track by dt.date" class="text-center" role="gridcell" id="{{::dt.uid}}" ng-class="::dt.customClass">\n <button type="button" style="min-width:100%;" class="btn btn-default" ng-class="{\'btn-info\': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="::{\'text-info\': dt.current}">{{::dt.label}}</span></button>\n </td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html",'<ul class="dropdown-menu" dropdown-nested ng-if="isOpen" style="display: block" ng-style="{top: position.top+\'px\', left: position.left+\'px\'}" ng-keydown="keydown($event)" ng-click="$event.stopPropagation()">\n <li ng-transclude></li>\n <li ng-if="showButtonBar" style="padding:10px 9px 2px">\n <span class="btn-group pull-left">\n <button type="button" class="btn btn-sm btn-info" ng-click="select(\'today\')" ng-disabled="isDisabled(\'today\')">{{ getText(\'current\') }}</button>\n <button type="button" class="btn btn-sm btn-danger" ng-click="select(null)">{{ getText(\'clear\') }}</button>\n </span>\n <button type="button" class="btn btn-sm btn-success pull-right" ng-click="close()">{{ getText(\'close\') }}</button>\n </li>\n</ul>\n')}]),angular.module("template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/year.html",'<table role="grid" aria-labelledby="{{::uniqueId}}-title" aria-activedescendant="{{activeDateId}}">\n <thead>\n <tr>\n <th><button type="button" class="btn btn-default btn-sm pull-left" ng-click="move(-1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-left"></i></button></th>\n <th colspan="3"><button id="{{::uniqueId}}-title" role="heading" aria-live="assertive" aria-atomic="true" type="button" class="btn btn-default btn-sm" ng-click="toggleMode()" ng-disabled="datepickerMode === maxMode" tabindex="-1" style="width:100%;"><strong>{{title}}</strong></button></th>\n <th><button type="button" class="btn btn-default btn-sm pull-right" ng-click="move(1)" tabindex="-1"><i class="glyphicon glyphicon-chevron-right"></i></button></th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat="row in rows track by $index">\n <td ng-repeat="dt in row track by dt.date" class="text-center" role="gridcell" id="{{::dt.uid}}" ng-class="::dt.customClass">\n <button type="button" style="min-width:100%;" class="btn btn-default" ng-class="{\'btn-info\': dt.selected, active: isActive(dt)}" ng-click="select(dt.date)" ng-disabled="dt.disabled" tabindex="-1"><span ng-class="::{\'text-info\': dt.current}">{{::dt.label}}</span></button>\n </td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'<div uib-modal-animation-class="fade"\n modal-in-class="in"\n ng-style="{\'z-index\': 1040 + (index && 1 || 0) + index*10}"\n></div>\n')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'<div modal-render="{{$isRendered}}" tabindex="-1" role="dialog" class="modal"\n uib-modal-animation-class="fade"\n modal-in-class="in"\n ng-style="{\'z-index\': 1050 + index*10, display: \'block\'}">\n <div class="modal-dialog" ng-class="size ? \'modal-\' + size : \'\'"><div class="modal-content" uib-modal-transclude></div></div>\n</div>\n')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'<ul class="pager">\n <li ng-class="{disabled: noPrevious()||ngDisabled, previous: align}"><a href ng-click="selectPage(page - 1, $event)">{{::getText(\'previous\')}}</a></li>\n <li ng-class="{disabled: noNext()||ngDisabled, next: align}"><a href ng-click="selectPage(page + 1, $event)">{{::getText(\'next\')}}</a></li>\n</ul>\n')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'<ul class="pagination">\n <li ng-if="::boundaryLinks" ng-class="{disabled: noPrevious()||ngDisabled}" class="pagination-first"><a href ng-click="selectPage(1, $event)">{{::getText(\'first\')}}</a></li>\n <li ng-if="::directionLinks" ng-class="{disabled: noPrevious()||ngDisabled}" class="pagination-prev"><a href ng-click="selectPage(page - 1, $event)">{{::getText(\'previous\')}}</a></li>\n <li ng-repeat="page in pages track by $index" ng-class="{active: page.active,disabled: ngDisabled&&!page.active}" class="pagination-page"><a href ng-click="selectPage(page.number, $event)">{{page.text}}</a></li>\n <li ng-if="::directionLinks" ng-class="{disabled: noNext()||ngDisabled}" class="pagination-next"><a href ng-click="selectPage(page + 1, $event)">{{::getText(\'next\')}}</a></li>\n <li ng-if="::boundaryLinks" ng-class="{disabled: noNext()||ngDisabled}" class="pagination-last"><a href ng-click="selectPage(totalPages, $event)">{{::getText(\'last\')}}</a></li>\n</ul>\n')}]),angular.module("template/tooltip/tooltip-html-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-popup.html",'<div\n tooltip-animation-class="fade"\n uib-tooltip-classes\n ng-class="{ in: isOpen() }">\n <div class="tooltip-arrow"></div>\n <div class="tooltip-inner" ng-bind-html="contentExp()"></div>\n</div>\n')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'<div class="tooltip"\n tooltip-animation-class="fade"\n tooltip-classes\n ng-class="{ in: isOpen() }">\n <div class="tooltip-arrow"></div>\n <div class="tooltip-inner" bind-html-unsafe="content"></div>\n</div>\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'<div\n tooltip-animation-class="fade"\n uib-tooltip-classes\n ng-class="{ in: isOpen() }">\n <div class="tooltip-arrow"></div>\n <div class="tooltip-inner" ng-bind="content"></div>\n</div>\n')}]),angular.module("template/tooltip/tooltip-template-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-template-popup.html",'<div\n tooltip-animation-class="fade"\n uib-tooltip-classes\n ng-class="{ in: isOpen() }">\n <div class="tooltip-arrow"></div>\n <div class="tooltip-inner"\n uib-tooltip-template-transclude="contentExp()"\n tooltip-template-transclude-scope="originScope()"></div>\n</div>\n')}]),angular.module("template/popover/popover-html.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover-html.html",'<div tooltip-animation-class="fade"\n uib-tooltip-classes\n ng-class="{ in: isOpen() }">\n <div class="arrow"></div>\n\n <div class="popover-inner">\n <h3 class="popover-title" ng-bind="title" ng-if="title"></h3>\n <div class="popover-content" ng-bind-html="contentExp()"></div>\n </div>\n</div>\n')}]),angular.module("template/popover/popover-template.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover-template.html",'<div tooltip-animation-class="fade"\n uib-tooltip-classes\n ng-class="{ in: isOpen() }">\n <div class="arrow"></div>\n\n <div class="popover-inner">\n <h3 class="popover-title" ng-bind="title" ng-if="title"></h3>\n <div class="popover-content"\n uib-tooltip-template-transclude="contentExp()"\n tooltip-template-transclude-scope="originScope()"></div>\n </div>\n</div>\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'<div tooltip-animation-class="fade"\n uib-tooltip-classes\n ng-class="{ in: isOpen() }">\n <div class="arrow"></div>\n\n <div class="popover-inner">\n <h3 class="popover-title" ng-bind="title" ng-if="title"></h3>\n <div class="popover-content" ng-bind="content"></div>\n </div>\n</div>\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'<div class="progress-bar" ng-class="type && \'progress-bar-\' + type" role="progressbar" aria-valuenow="{{value}}" aria-valuemin="0" aria-valuemax="{{max}}" ng-style="{width: (percent < 100 ? percent : 100) + \'%\'}" aria-valuetext="{{percent | number:0}}%" aria-labelledby="{{::title}}" style="min-width: 0;" ng-transclude></div>\n')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'<div class="progress" ng-transclude aria-labelledby="{{::title}}"></div>')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'<div class="progress">\n <div class="progress-bar" ng-class="type && \'progress-bar-\' + type" role="progressbar" aria-valuenow="{{value}}" aria-valuemin="0" aria-valuemax="{{max}}" ng-style="{width: (percent < 100 ? percent : 100) + \'%\'}" aria-valuetext="{{percent | number:0}}%" aria-labelledby="{{::title}}" style="min-width: 0;" ng-transclude></div>\n</div>\n')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'<span ng-mouseleave="reset()" ng-keydown="onKeydown($event)" tabindex="0" role="slider" aria-valuemin="0" aria-valuemax="{{range.length}}" aria-valuenow="{{value}}">\n <span ng-repeat-start="r in range track by $index" class="sr-only">({{ $index < value ? \'*\' : \' \' }})</span>\n <i ng-repeat-end ng-mouseenter="enter($index + 1)" ng-click="rate($index + 1)" class="glyphicon" ng-class="$index < value && (r.stateOn || \'glyphicon-star\') || (r.stateOff || \'glyphicon-star-empty\')" ng-attr-title="{{r.title}}" aria-valuetext="{{r.title}}"></i>\n</span>\n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'<li ng-class="{active: active, disabled: disabled}">\n <a href ng-click="select()" uib-tab-heading-transclude>{{heading}}</a>\n</li>\n')}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){
11 a.put("template/tabs/tabset.html",'<div>\n <ul class="nav nav-{{type || \'tabs\'}}" ng-class="{\'nav-stacked\': vertical, \'nav-justified\': justified}" ng-transclude></ul>\n <div class="tab-content">\n <div class="tab-pane" \n ng-repeat="tab in tabs" \n ng-class="{active: tab.active}"\n uib-tab-content-transclude="tab">\n </div>\n </div>\n</div>\n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'<table>\n <tbody>\n <tr class="text-center" ng-show="::showSpinners">\n <td><a ng-click="incrementHours()" ng-class="{disabled: noIncrementHours()}" class="btn btn-link" ng-disabled="noIncrementHours()" tabindex="{{::tabindex}}"><span class="glyphicon glyphicon-chevron-up"></span></a></td>\n <td>&nbsp;</td>\n <td><a ng-click="incrementMinutes()" ng-class="{disabled: noIncrementMinutes()}" class="btn btn-link" ng-disabled="noIncrementMinutes()" tabindex="{{::tabindex}}"><span class="glyphicon glyphicon-chevron-up"></span></a></td>\n <td ng-show="showMeridian"></td>\n </tr>\n <tr>\n <td class="form-group" ng-class="{\'has-error\': invalidHours}">\n <input style="width:50px;" type="text" ng-model="hours" ng-change="updateHours()" class="form-control text-center" ng-readonly="::readonlyInput" maxlength="2" tabindex="{{::tabindex}}">\n </td>\n <td>:</td>\n <td class="form-group" ng-class="{\'has-error\': invalidMinutes}">\n <input style="width:50px;" type="text" ng-model="minutes" ng-change="updateMinutes()" class="form-control text-center" ng-readonly="::readonlyInput" maxlength="2" tabindex="{{::tabindex}}">\n </td>\n <td ng-show="showMeridian"><button type="button" ng-class="{disabled: noToggleMeridian()}" class="btn btn-default text-center" ng-click="toggleMeridian()" ng-disabled="noToggleMeridian()" tabindex="{{::tabindex}}">{{meridian}}</button></td>\n </tr>\n <tr class="text-center" ng-show="::showSpinners">\n <td><a ng-click="decrementHours()" ng-class="{disabled: noDecrementHours()}" class="btn btn-link" ng-disabled="noDecrementHours()" tabindex="{{::tabindex}}"><span class="glyphicon glyphicon-chevron-down"></span></a></td>\n <td>&nbsp;</td>\n <td><a ng-click="decrementMinutes()" ng-class="{disabled: noDecrementMinutes()}" class="btn btn-link" ng-disabled="noDecrementMinutes()" tabindex="{{::tabindex}}"><span class="glyphicon glyphicon-chevron-down"></span></a></td>\n <td ng-show="showMeridian"></td>\n </tr>\n </tbody>\n</table>\n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'<a href tabindex="-1" ng-bind-html="match.label | uibTypeaheadHighlight:query"></a>\n')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html",'<ul class="dropdown-menu" ng-show="isOpen() && !moveInProgress" ng-style="{top: position().top+\'px\', left: position().left+\'px\'}" style="display: block;" role="listbox" aria-hidden="{{!isOpen()}}">\n <li ng-repeat="match in matches track by $index" ng-class="{active: isActive($index) }" ng-mouseenter="selectActive($index)" ng-click="selectMatch($index)" role="option" id="{{::match.id}}">\n <div uib-typeahead-match index="$index" match="match" query="query" template-url="templateUrl"></div>\n </li>\n</ul>\n')}]),!angular.$$csp()&&angular.element(document).find("head").prepend('<style type="text/css">.ng-animate.item:not(.left):not(.right){-webkit-transition:0s ease-in-out left;transition:0s ease-in-out left}</style>');
0 /*!
1 * ui-grid - v3.0.7 - 2015-10-06
2 * Copyright (c) 2015 ; License: MIT
3 */
4 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:before,
5 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:after,
6 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:before,
7 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:after,
8 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:before,
9 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:after {
10 content: " ";
11 display: table;
12 }
13 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:after,
14 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:after,
15 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:after {
16 clear: both;
17 }
18 .ui-grid {
19 box-sizing: content-box;
20 -webkit-border-radius: 0px;
21 -moz-border-radius: 0px;
22 border-radius: 0px;
23 -webkit-transform: translateZ(0);
24 -moz-transform: translateZ(0);
25 -o-transform: translateZ(0);
26 -ms-transform: translateZ(0);
27 transform: translateZ(0);
28 }
29 .ui-grid-vertical-bar {
30 position: absolute;
31 right: 0;
32 width: 0;
33 }
34 .ui-grid-header-cell:not(:last-child) .ui-grid-vertical-bar,
35 .ui-grid-cell:not(:last-child) .ui-grid-vertical-bar {
36 width: 1px;
37 }
38 .ui-grid-scrollbar-placeholder {
39 background-color: transparent;
40 }
41 .ui-grid-header-cell:not(:last-child) .ui-grid-vertical-bar {
42 background-color: #d4d4d4;
43 }
44 .ui-grid-cell:not(:last-child) .ui-grid-vertical-bar {
45 background-color: #d4d4d4;
46 }
47 .ui-grid-header-cell:last-child .ui-grid-vertical-bar {
48 right: -1px;
49 width: 1px;
50 background-color: #d4d4d4;
51 }
52 .ui-grid-clearfix:before,
53 .ui-grid-clearfix:after {
54 content: "";
55 display: table;
56 }
57 .ui-grid-clearfix:after {
58 clear: both;
59 }
60 .ui-grid-invisible {
61 visibility: hidden;
62 }
63 .ui-grid-contents-wrapper {
64 position: relative;
65 height: 100%;
66 width: 100%;
67 }
68 .ui-grid-sr-only {
69 position: absolute;
70 width: 1px;
71 height: 1px;
72 margin: -1px;
73 padding: 0;
74 overflow: hidden;
75 clip: rect(0, 0, 0, 0);
76 border: 0;
77 }
78 .ui-grid-top-panel-background {
79 background: #f3f3f3;
80 background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(1, #ffffff));
81 background: -ms-linear-gradient(bottom, #eeeeee, #ffffff);
82 background: -moz-linear-gradient(center bottom, #eeeeee 0%, #ffffff 100%);
83 background: -o-linear-gradient(#ffffff, #eeeeee);
84 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
85 }
86 .ui-grid-header {
87 border-bottom: 1px solid #d4d4d4;
88 background-image: linear-gradient(to bottom,#E8EFF0 0,#DDDDDD 100%);
89 background-image: -webkit-linear-gradient(top,#E8EFF0 0,#DDDDDD 100%);
90 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#E8EFF0', endColorstr='#DDDDDD', GradientType=0);
91 filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
92 background-repeat: repeat-x;
93 border: none;
94 font-size: 14px;
95 padding: 10px 5px;
96 letter-spacing: -.05em;
97 font-weight: normal;
98 text-transform: uppercase;
99 }
100 .ui-grid-top-panel {
101 position: relative;
102 overflow: hidden;
103 font-weight: bold;
104 border-top-right-radius: -1px;
105 border-bottom-right-radius: 0;
106 border-bottom-left-radius: 0;
107 border-top-left-radius: -1px;
108 -moz-background-clip: padding-box;
109 -webkit-background-clip: padding-box;
110 background-clip: padding-box;
111 }
112 .ui-grid-header-viewport {
113 overflow: hidden;
114 }
115 .ui-grid-header-canvas:before,
116 .ui-grid-header-canvas:after {
117 content: "";
118 display: table;
119 line-height: 0;
120 }
121 .ui-grid-header-canvas:after {
122 clear: both;
123 }
124 .ui-grid-header-cell-wrapper {
125 position: relative;
126 display: table;
127 box-sizing: border-box;
128 height: 100%;
129 }
130 .ui-grid-header-cell-row {
131 display: table-row;
132 position: relative;
133 }
134 .ui-grid-header-cell {
135 position: relative;
136 box-sizing: border-box;
137 background-color: inherit;
138 border-color: #d4d4d4;
139 display: table-cell;
140 -webkit-user-select: none;
141 -moz-user-select: none;
142 -ms-user-select: none;
143 user-select: none;
144 width: 0;
145 }
146 .ui-grid-header-cell {
147 color: #3a3a3a;
148 text-shadow: 0 1px 0 #fff;
149 font-weight: normal;
150 }
151 .ui-grid-header-cell:last-child {
152 border-right: 0;
153 }
154 .ui-grid-header-cell .sortable {
155 cursor: pointer;
156 }
157 .ui-grid-header-cell .ui-grid-sort-priority-number {
158 margin-left: -8px;
159 }
160 .ui-grid-header .ui-grid-vertical-bar {
161 top: 0;
162 bottom: 0;
163 }
164 .ui-grid-column-menu-button {
165 position: absolute;
166 right: 1px;
167 top: 0;
168 }
169 .ui-grid-column-menu-button .ui-grid-icon-angle-down {
170 vertical-align: sub;
171 }
172 .ui-grid-column-menu-button-last-col {
173 margin-right: 25px;
174 }
175 .ui-grid-column-menu {
176 position: absolute;
177 }
178 /* Slide up/down animations */
179 .ui-grid-column-menu .ui-grid-menu .ui-grid-menu-mid.ng-hide-add,
180 .ui-grid-column-menu .ui-grid-menu .ui-grid-menu-mid.ng-hide-remove {
181 -webkit-transition: all 0.05s linear;
182 -moz-transition: all 0.05s linear;
183 -o-transition: all 0.05s linear;
184 transition: all 0.05s linear;
185 display: block !important;
186 }
187 .ui-grid-column-menu .ui-grid-menu .ui-grid-menu-mid.ng-hide-add.ng-hide-add-active,
188 .ui-grid-column-menu .ui-grid-menu .ui-grid-menu-mid.ng-hide-remove {
189 -webkit-transform: translateY(-100%);
190 -moz-transform: translateY(-100%);
191 -o-transform: translateY(-100%);
192 -ms-transform: translateY(-100%);
193 transform: translateY(-100%);
194 }
195 .ui-grid-column-menu .ui-grid-menu .ui-grid-menu-mid.ng-hide-add,
196 .ui-grid-column-menu .ui-grid-menu .ui-grid-menu-mid.ng-hide-remove.ng-hide-remove-active {
197 -webkit-transform: translateY(0);
198 -moz-transform: translateY(0);
199 -o-transform: translateY(0);
200 -ms-transform: translateY(0);
201 transform: translateY(0);
202 }
203 /* Slide up/down animations */
204 .ui-grid-menu-button .ui-grid-menu .ui-grid-menu-mid.ng-hide-add,
205 .ui-grid-menu-button .ui-grid-menu .ui-grid-menu-mid.ng-hide-remove {
206 -webkit-transition: all 0.05s linear;
207 -moz-transition: all 0.05s linear;
208 -o-transition: all 0.05s linear;
209 transition: all 0.05s linear;
210 display: block !important;
211 }
212 .ui-grid-menu-button .ui-grid-menu .ui-grid-menu-mid.ng-hide-add.ng-hide-add-active,
213 .ui-grid-menu-button .ui-grid-menu .ui-grid-menu-mid.ng-hide-remove {
214 -webkit-transform: translateY(-100%);
215 -moz-transform: translateY(-100%);
216 -o-transform: translateY(-100%);
217 -ms-transform: translateY(-100%);
218 transform: translateY(-100%);
219 }
220 .ui-grid-menu-button .ui-grid-menu .ui-grid-menu-mid.ng-hide-add,
221 .ui-grid-menu-button .ui-grid-menu .ui-grid-menu-mid.ng-hide-remove.ng-hide-remove-active {
222 -webkit-transform: translateY(0);
223 -moz-transform: translateY(0);
224 -o-transform: translateY(0);
225 -ms-transform: translateY(0);
226 transform: translateY(0);
227 }
228 .ui-grid-filter-container {
229 padding: 4px 10px;
230 position: relative;
231 }
232 .ui-grid-filter-container .ui-grid-filter-button {
233 position: absolute;
234 top: 0;
235 bottom: 0;
236 right: 0;
237 }
238 .ui-grid-filter-container .ui-grid-filter-button [class^="ui-grid-icon"] {
239 position: absolute;
240 top: 50%;
241 line-height: 32px;
242 margin-top: -16px;
243 right: 10px;
244 opacity: 0.66;
245 }
246 .ui-grid-filter-container .ui-grid-filter-button [class^="ui-grid-icon"]:hover {
247 opacity: 1;
248 }
249 .ui-grid-filter-container .ui-grid-filter-button-select {
250 position: absolute;
251 top: 0;
252 bottom: 0;
253 right: 0;
254 }
255 .ui-grid-filter-container .ui-grid-filter-button-select [class^="ui-grid-icon"] {
256 position: absolute;
257 top: 50%;
258 line-height: 32px;
259 margin-top: -16px;
260 right: 0px;
261 opacity: 0.66;
262 }
263 .ui-grid-filter-container .ui-grid-filter-button-select [class^="ui-grid-icon"]:hover {
264 opacity: 1;
265 }
266 input[type="text"].ui-grid-filter-input {
267 padding: 0;
268 margin: 0;
269 border: 0;
270 width: 100%;
271 border: 1px solid #d4d4d4;
272 -webkit-border-top-right-radius: 0px;
273 -webkit-border-bottom-right-radius: 0;
274 -webkit-border-bottom-left-radius: 0;
275 -webkit-border-top-left-radius: 0;
276 -moz-border-radius-topright: 0px;
277 -moz-border-radius-bottomright: 0;
278 -moz-border-radius-bottomleft: 0;
279 -moz-border-radius-topleft: 0;
280 border-top-right-radius: 0px;
281 border-bottom-right-radius: 0;
282 border-bottom-left-radius: 0;
283 border-top-left-radius: 0;
284 -moz-background-clip: padding-box;
285 -webkit-background-clip: padding-box;
286 background-clip: padding-box;
287 }
288 input[type="text"].ui-grid-filter-input:hover {
289 border: 1px solid #d4d4d4;
290 }
291 select.ui-grid-filter-select {
292 padding: 0;
293 margin: 0;
294 border: 0;
295 width: 90%;
296 border: 1px solid #d4d4d4;
297 -webkit-border-top-right-radius: 0px;
298 -webkit-border-bottom-right-radius: 0;
299 -webkit-border-bottom-left-radius: 0;
300 -webkit-border-top-left-radius: 0;
301 -moz-border-radius-topright: 0px;
302 -moz-border-radius-bottomright: 0;
303 -moz-border-radius-bottomleft: 0;
304 -moz-border-radius-topleft: 0;
305 border-top-right-radius: 0px;
306 border-bottom-right-radius: 0;
307 border-bottom-left-radius: 0;
308 border-top-left-radius: 0;
309 -moz-background-clip: padding-box;
310 -webkit-background-clip: padding-box;
311 background-clip: padding-box;
312 }
313 select.ui-grid-filter-select:hover {
314 border: 1px solid #d4d4d4;
315 }
316 .ui-grid-filter-cancel-button-hidden select.ui-grid-filter-select {
317 width: 100%;
318 }
319 .ui-grid-render-container {
320 position: inherit;
321 -webkit-border-top-right-radius: 0;
322 -webkit-border-bottom-right-radius: 0px;
323 -webkit-border-bottom-left-radius: 0px;
324 -webkit-border-top-left-radius: 0;
325 -moz-border-radius-topright: 0;
326 -moz-border-radius-bottomright: 0px;
327 -moz-border-radius-bottomleft: 0px;
328 -moz-border-radius-topleft: 0;
329 border-top-right-radius: 0;
330 border-bottom-right-radius: 0px;
331 border-bottom-left-radius: 0px;
332 border-top-left-radius: 0;
333 -moz-background-clip: padding-box;
334 -webkit-background-clip: padding-box;
335 background-clip: padding-box;
336 }
337 .ui-grid-render-container:focus {
338 outline: none;
339 }
340 .ui-grid-viewport {
341 min-height: 20px;
342 position: relative;
343 overflow-y: scroll;
344 -webkit-overflow-scrolling: touch;
345 }
346 .ui-grid-viewport:focus {
347 outline: none !important;
348 }
349 .ui-grid-canvas {
350 position: relative;
351 padding-top: 1px;
352 }
353 .ui-grid-row:nth-child(odd) .ui-grid-cell {
354 background-color: #fdfdfd;
355 }
356 .ui-grid-row:nth-child(even) .ui-grid-cell {
357 background-color: #f1f1f1;
358 }
359 .ui-grid-row:last-child .ui-grid-cell {
360 border-bottom-color: #d4d4d4;
361 border-bottom-style: solid;
362 }
363 .ui-grid-no-row-overlay {
364 position: absolute;
365 top: 0;
366 bottom: 0;
367 left: 0;
368 right: 0;
369 margin: 10%;
370 background: #f3f3f3;
371 background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(1, #ffffff));
372 background: -ms-linear-gradient(bottom, #eeeeee, #ffffff);
373 background: -moz-linear-gradient(center bottom, #eeeeee 0%, #ffffff 100%);
374 background: -o-linear-gradient(#ffffff, #eeeeee);
375 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
376 -webkit-border-top-right-radius: 0px;
377 -webkit-border-bottom-right-radius: 0;
378 -webkit-border-bottom-left-radius: 0;
379 -webkit-border-top-left-radius: 0;
380 -moz-border-radius-topright: 0px;
381 -moz-border-radius-bottomright: 0;
382 -moz-border-radius-bottomleft: 0;
383 -moz-border-radius-topleft: 0;
384 border-top-right-radius: 0px;
385 border-bottom-right-radius: 0;
386 border-bottom-left-radius: 0;
387 border-top-left-radius: 0;
388 -moz-background-clip: padding-box;
389 -webkit-background-clip: padding-box;
390 background-clip: padding-box;
391 border: 1px solid #d4d4d4;
392 font-size: 2em;
393 text-align: center;
394 }
395 .ui-grid-no-row-overlay > * {
396 position: absolute;
397 display: table;
398 margin: auto 0;
399 width: 100%;
400 top: 0;
401 bottom: 0;
402 left: 0;
403 right: 0;
404 opacity: 0.66;
405 }
406 .ui-grid-cell {
407 overflow: hidden;
408 float: left;
409 background-color: inherit;
410 -webkit-box-sizing: border-box;
411 -moz-box-sizing: border-box;
412 box-sizing: border-box;
413 border-top: 1px solid #ddd;
414 }
415 .ui-grid-cell:last-child {
416 border-right: 0;
417 }
418 .ui-grid-cell-contents {
419 padding: 5px;
420 -moz-box-sizing: border-box;
421 -webkit-box-sizing: border-box;
422 box-sizing: border-box;
423 white-space: nowrap;
424 -ms-text-overflow: ellipsis;
425 -o-text-overflow: ellipsis;
426 text-overflow: ellipsis;
427 overflow: hidden;
428 height: 100%;
429 font-size: 12px;
430 }
431 .ui-grid-cell-contents-hidden {
432 visibility: hidden;
433 width: 0;
434 height: 0;
435 display: none;
436 }
437 .ui-grid-footer-panel-background {
438 background: #f3f3f3;
439 background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(1, #ffffff));
440 background: -ms-linear-gradient(bottom, #eeeeee, #ffffff);
441 background: -moz-linear-gradient(center bottom, #eeeeee 0%, #ffffff 100%);
442 background: -o-linear-gradient(#ffffff, #eeeeee);
443 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
444 }
445 .ui-grid-footer-panel {
446 position: relative;
447 border-bottom: 1px solid #d4d4d4;
448 border-top: 1px solid #d4d4d4;
449 overflow: hidden;
450 font-weight: bold;
451 background: #f3f3f3;
452 background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(1, #ffffff));
453 background: -ms-linear-gradient(bottom, #eeeeee, #ffffff);
454 background: -moz-linear-gradient(center bottom, #eeeeee 0%, #ffffff 100%);
455 background: -o-linear-gradient(#ffffff, #eeeeee);
456 filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
457 -webkit-border-top-right-radius: -1px;
458 -webkit-border-bottom-right-radius: 0;
459 -webkit-border-bottom-left-radius: 0;
460 -webkit-border-top-left-radius: -1px;
461 -moz-border-radius-topright: -1px;
462 -moz-border-radius-bottomright: 0;
463 -moz-border-radius-bottomleft: 0;
464 -moz-border-radius-topleft: -1px;
465 border-top-right-radius: -1px;
466 border-bottom-right-radius: 0;
467 border-bottom-left-radius: 0;
468 border-top-left-radius: -1px;
469 -moz-background-clip: padding-box;
470 -webkit-background-clip: padding-box;
471 background-clip: padding-box;
472 }
473 .ui-grid-grid-footer {
474 float: left;
475 width: 100%;
476 }
477 .ui-grid-footer-viewport {
478 overflow: hidden;
479 }
480 .ui-grid-footer-canvas {
481 position: relative;
482 }
483 .ui-grid-footer-canvas:before,
484 .ui-grid-footer-canvas:after {
485 content: "";
486 display: table;
487 line-height: 0;
488 }
489 .ui-grid-footer-canvas:after {
490 clear: both;
491 }
492 .ui-grid-footer-cell-wrapper {
493 position: relative;
494 display: table;
495 box-sizing: border-box;
496 height: 100%;
497 }
498 .ui-grid-footer-cell-row {
499 display: table-row;
500 }
501 .ui-grid-footer-cell {
502 overflow: hidden;
503 background-color: inherit;
504 border-right: 1px solid;
505 border-color: #d4d4d4;
506 box-sizing: border-box;
507 display: table-cell;
508 }
509 .ui-grid-footer-cell:last-child {
510 border-right: 0;
511 }
512 input[type="text"].ui-grid-filter-input {
513 padding: 0;
514 margin: 0;
515 border: 0;
516 width: 100%;
517 border: 1px solid #d4d4d4;
518 -webkit-border-top-right-radius: 0px;
519 -webkit-border-bottom-right-radius: 0;
520 -webkit-border-bottom-left-radius: 0;
521 -webkit-border-top-left-radius: 0;
522 -moz-border-radius-topright: 0px;
523 -moz-border-radius-bottomright: 0;
524 -moz-border-radius-bottomleft: 0;
525 -moz-border-radius-topleft: 0;
526 border-top-right-radius: 0px;
527 border-bottom-right-radius: 0;
528 border-bottom-left-radius: 0;
529 border-top-left-radius: 0;
530 -moz-background-clip: padding-box;
531 -webkit-background-clip: padding-box;
532 background-clip: padding-box;
533 }
534 input[type="text"].ui-grid-filter-input:hover {
535 border: 1px solid #d4d4d4;
536 }
537 .ui-grid-menu-button {
538 z-index: 2;
539 position: absolute;
540 right: 0;
541 top: 0;
542 background: #f3f3f3;
543 border: 1px solid #d4d4d4;
544 cursor: pointer;
545 height: 31px;
546 font-weight: normal;
547 }
548 .ui-grid-menu-button .ui-grid-icon-container {
549 margin-top: 3px;
550 }
551 .ui-grid-menu-button .ui-grid-menu {
552 right: 0;
553 }
554 .ui-grid-menu-button .ui-grid-menu .ui-grid-menu-mid {
555 overflow: scroll;
556 max-height: 300px;
557 border: 1px solid #d4d4d4;
558 }
559 .ui-grid-menu {
560 z-index: 2;
561 position: absolute;
562 padding: 0 10px 20px 10px;
563 cursor: pointer;
564 box-sizing: border-box;
565 }
566 .ui-grid-menu .ui-grid-menu-inner {
567 background: #f3f3f3;
568 border: 1px solid #d4d4d4;
569 position: relative;
570 white-space: nowrap;
571 -webkit-border-radius: 0px;
572 -moz-border-radius: 0px;
573 border-radius: 0px;
574 -webkit-box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2), inset 0 12px 12px -14px rgba(0, 0, 0, 0.2);
575 -moz-box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2), inset 0 12px 12px -14px rgba(0, 0, 0, 0.2);
576 box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2), inset 0 12px 12px -14px rgba(0, 0, 0, 0.2);
577 }
578 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button {
579 position: absolute;
580 right: 0px;
581 top: 0px;
582 display: inline-block;
583 margin-bottom: 0;
584 font-weight: normal;
585 text-align: center;
586 vertical-align: middle;
587 touch-action: manipulation;
588 cursor: pointer;
589 background-image: none;
590 border: 1px solid transparent;
591 white-space: nowrap;
592 padding: 6px 12px;
593 font-size: 14px;
594 line-height: 1.42857143;
595 border-radius: 4px;
596 -webkit-user-select: none;
597 -moz-user-select: none;
598 -ms-user-select: none;
599 user-select: none;
600 padding: 1px 1px;
601 font-size: 10px;
602 line-height: 1;
603 border-radius: 2px;
604 color: transparent;
605 background-color: transparent;
606 border-color: transparent;
607 }
608 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:focus,
609 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active:focus,
610 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active:focus,
611 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.focus,
612 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active.focus,
613 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active.focus {
614 outline: thin dotted;
615 outline: 5px auto -webkit-focus-ring-color;
616 outline-offset: -2px;
617 }
618 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:hover,
619 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:focus,
620 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.focus {
621 color: #333333;
622 text-decoration: none;
623 }
624 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active,
625 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active {
626 outline: 0;
627 background-image: none;
628 -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
629 box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
630 }
631 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled,
632 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button[disabled],
633 fieldset[disabled] .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button {
634 cursor: not-allowed;
635 opacity: 0.65;
636 filter: alpha(opacity=65);
637 -webkit-box-shadow: none;
638 box-shadow: none;
639 }
640 a.ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled,
641 fieldset[disabled] a.ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button {
642 pointer-events: none;
643 }
644 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:focus,
645 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.focus {
646 color: transparent;
647 background-color: rgba(0, 0, 0, 0);
648 border-color: rgba(0, 0, 0, 0);
649 }
650 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:hover {
651 color: transparent;
652 background-color: rgba(0, 0, 0, 0);
653 border-color: rgba(0, 0, 0, 0);
654 }
655 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active,
656 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active,
657 .open > .dropdown-toggle.ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button {
658 color: transparent;
659 background-color: rgba(0, 0, 0, 0);
660 border-color: rgba(0, 0, 0, 0);
661 }
662 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active:hover,
663 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active:hover,
664 .open > .dropdown-toggle.ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:hover,
665 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active:focus,
666 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active:focus,
667 .open > .dropdown-toggle.ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:focus,
668 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active.focus,
669 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active.focus,
670 .open > .dropdown-toggle.ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.focus {
671 color: transparent;
672 background-color: rgba(0, 0, 0, 0);
673 border-color: rgba(0, 0, 0, 0);
674 }
675 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active,
676 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active,
677 .open > .dropdown-toggle.ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button {
678 background-image: none;
679 }
680 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled,
681 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button[disabled],
682 fieldset[disabled] .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button,
683 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled:hover,
684 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button[disabled]:hover,
685 fieldset[disabled] .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:hover,
686 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled:focus,
687 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button[disabled]:focus,
688 fieldset[disabled] .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:focus,
689 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled.focus,
690 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button[disabled].focus,
691 fieldset[disabled] .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.focus,
692 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled:active,
693 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button[disabled]:active,
694 fieldset[disabled] .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button:active,
695 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.disabled.active,
696 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button[disabled].active,
697 fieldset[disabled] .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button.active {
698 background-color: transparent;
699 border-color: transparent;
700 }
701 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button .badge {
702 color: transparent;
703 background-color: transparent;
704 }
705 .ui-grid-menu .ui-grid-menu-inner .ui-grid-menu-close-button > i {
706 opacity: 0.75;
707 color: black;
708 }
709 .ui-grid-menu .ui-grid-menu-inner ul {
710 margin: 0;
711 padding: 0;
712 list-style-type: none;
713 }
714 .ui-grid-menu .ui-grid-menu-inner ul li {
715 padding: 0px;
716 }
717 .ui-grid-menu .ui-grid-menu-inner ul li button {
718 min-width: 100%;
719 padding: 8px;
720 text-align: left;
721 background: transparent;
722 border: none;
723 }
724 .ui-grid-menu .ui-grid-menu-inner ul li button:hover,
725 .ui-grid-menu .ui-grid-menu-inner ul li button:focus {
726 -webkit-box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2);
727 -moz-box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2);
728 box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2);
729 }
730 .ui-grid-menu .ui-grid-menu-inner ul li button.ui-grid-menu-item-active {
731 -webkit-box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2);
732 -moz-box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2);
733 box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2);
734 background-color: #cecece;
735 }
736 .ui-grid-menu .ui-grid-menu-inner ul li:not(:last-child) > button {
737 border-bottom: 1px solid #d4d4d4;
738 }
739 .ui-grid-sortarrow {
740 right: 5px;
741 position: absolute;
742 width: 20px;
743 top: 0;
744 bottom: 0;
745 background-position: center;
746 }
747 .ui-grid-sortarrow.down {
748 -webkit-transform: rotate(180deg);
749 -moz-transform: rotate(180deg);
750 -o-transform: rotate(180deg);
751 -ms-transform: rotate(180deg);
752 transform: rotate(180deg);
753 }
754 @font-face {
755 font-family: 'ui-grid';
756 src: url('ui-grid.eot');
757 src: url('ui-grid.eot#iefix') format('embedded-opentype'), url('ui-grid.woff') format('woff'), url('ui-grid.ttf') format('truetype'), url('ui-grid.svg?#ui-grid') format('svg');
758 font-weight: normal;
759 font-style: normal;
760 }
761 /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
762 /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
763 /*
764 @media screen and (-webkit-min-device-pixel-ratio:0) {
765 @font-face {
766 font-family: 'ui-grid';
767 src: url('../font/ui-grid.svg?12312827#ui-grid') format('svg');
768 }
769 }
770 */
771 [class^="ui-grid-icon"]:before,
772 [class*=" ui-grid-icon"]:before {
773 font-family: 'ui-grid';
774 font-style: normal;
775 font-weight: normal;
776 speak: none;
777 display: inline-block;
778 text-decoration: inherit;
779 width: 1em;
780 margin-right: .2em;
781 text-align: center;
782 /* opacity: .8; */
783 /* For safety - reset parent styles, that can break glyph codes*/
784 font-variant: normal;
785 text-transform: none;
786 /* fix buttons height, for twitter bootstrap */
787 line-height: 1em;
788 /* Animation center compensation - margins should be symmetric */
789 /* remove if not needed */
790 margin-left: .2em;
791 /* you can be more comfortable with increased icons size */
792 /* font-size: 120%; */
793 /* Uncomment for 3D effect */
794 /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
795 }
796 .ui-grid-icon-blank::before {
797 width: 1em;
798 content: ' ';
799 }
800 /*
801 * RTL Styles
802 */
803 .ui-grid[dir=rtl] .ui-grid-header-cell,
804 .ui-grid[dir=rtl] .ui-grid-footer-cell,
805 .ui-grid[dir=rtl] .ui-grid-cell {
806 float: right !important;
807 }
808 .ui-grid[dir=rtl] .ui-grid-column-menu-button {
809 position: absolute;
810 left: 1px;
811 top: 0;
812 right: inherit;
813 }
814 .ui-grid[dir=rtl] .ui-grid-cell:first-child,
815 .ui-grid[dir=rtl] .ui-grid-header-cell:first-child,
816 .ui-grid[dir=rtl] .ui-grid-footer-cell:first-child {
817 border-right: 0;
818 }
819 .ui-grid[dir=rtl] .ui-grid-cell:last-child,
820 .ui-grid[dir=rtl] .ui-grid-header-cell:last-child {
821 border-right: 1px solid #d4d4d4;
822 border-left: 0;
823 }
824 .ui-grid[dir=rtl] .ui-grid-header-cell:first-child .ui-grid-vertical-bar,
825 .ui-grid[dir=rtl] .ui-grid-footer-cell:first-child .ui-grid-vertical-bar,
826 .ui-grid[dir=rtl] .ui-grid-cell:first-child .ui-grid-vertical-bar {
827 width: 0;
828 }
829 .ui-grid[dir=rtl] .ui-grid-menu-button {
830 z-index: 2;
831 position: absolute;
832 left: 0;
833 right: auto;
834 background: #f3f3f3;
835 border: 1px solid #d4d4d4;
836 cursor: pointer;
837 min-height: 27px;
838 font-weight: normal;
839 }
840 .ui-grid[dir=rtl] .ui-grid-menu-button .ui-grid-menu {
841 left: 0;
842 right: auto;
843 }
844 .ui-grid[dir=rtl] .ui-grid-filter-container .ui-grid-filter-button {
845 right: initial;
846 left: 0;
847 }
848 .ui-grid[dir=rtl] .ui-grid-filter-container .ui-grid-filter-button [class^="ui-grid-icon"] {
849 right: initial;
850 left: 10px;
851 }
852 /*
853 Animation example, for spinners
854 */
855 .ui-grid-animate-spin {
856 -moz-animation: ui-grid-spin 2s infinite linear;
857 -o-animation: ui-grid-spin 2s infinite linear;
858 -webkit-animation: ui-grid-spin 2s infinite linear;
859 animation: ui-grid-spin 2s infinite linear;
860 display: inline-block;
861 }
862 @-moz-keyframes ui-grid-spin {
863 0% {
864 -moz-transform: rotate(0deg);
865 -o-transform: rotate(0deg);
866 -webkit-transform: rotate(0deg);
867 transform: rotate(0deg);
868 }
869 100% {
870 -moz-transform: rotate(359deg);
871 -o-transform: rotate(359deg);
872 -webkit-transform: rotate(359deg);
873 transform: rotate(359deg);
874 }
875 }
876 @-webkit-keyframes ui-grid-spin {
877 0% {
878 -moz-transform: rotate(0deg);
879 -o-transform: rotate(0deg);
880 -webkit-transform: rotate(0deg);
881 transform: rotate(0deg);
882 }
883 100% {
884 -moz-transform: rotate(359deg);
885 -o-transform: rotate(359deg);
886 -webkit-transform: rotate(359deg);
887 transform: rotate(359deg);
888 }
889 }
890 @-o-keyframes ui-grid-spin {
891 0% {
892 -moz-transform: rotate(0deg);
893 -o-transform: rotate(0deg);
894 -webkit-transform: rotate(0deg);
895 transform: rotate(0deg);
896 }
897 100% {
898 -moz-transform: rotate(359deg);
899 -o-transform: rotate(359deg);
900 -webkit-transform: rotate(359deg);
901 transform: rotate(359deg);
902 }
903 }
904 @-ms-keyframes ui-grid-spin {
905 0% {
906 -moz-transform: rotate(0deg);
907 -o-transform: rotate(0deg);
908 -webkit-transform: rotate(0deg);
909 transform: rotate(0deg);
910 }
911 100% {
912 -moz-transform: rotate(359deg);
913 -o-transform: rotate(359deg);
914 -webkit-transform: rotate(359deg);
915 transform: rotate(359deg);
916 }
917 }
918 @keyframes ui-grid-spin {
919 0% {
920 -moz-transform: rotate(0deg);
921 -o-transform: rotate(0deg);
922 -webkit-transform: rotate(0deg);
923 transform: rotate(0deg);
924 }
925 100% {
926 -moz-transform: rotate(359deg);
927 -o-transform: rotate(359deg);
928 -webkit-transform: rotate(359deg);
929 transform: rotate(359deg);
930 }
931 }
932 /*---------------------------------------------------
933 LESS Elements 0.9
934 ---------------------------------------------------
935 A set of useful LESS mixins
936 More info at: http://lesselements.com
937 ---------------------------------------------------*/
938 /* This file contains variable declarations (do not remove this line) */
939 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
940 /**
941 * @section Grid styles
942 */
943 /**
944 * @section Header styles
945 */
946 /** @description Colors for header gradient */
947 /**
948 * @section Grid body styles
949 */
950 /** @description Colors used for row alternation */
951 /**
952 * @section Sort arrow colors
953 */
954 /**
955 * @section Scrollbar styles
956 */
957 /**
958 * @section font library path
959 */
960 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
961
962 /* This file contains variable declarations (do not remove this line) */
963 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
964 /**
965 * @section Grid styles
966 */
967 /**
968 * @section Header styles
969 */
970 /** @description Colors for header gradient */
971 /**
972 * @section Grid body styles
973 */
974 /** @description Colors used for row alternation */
975 /**
976 * @section Sort arrow colors
977 */
978 /**
979 * @section Scrollbar styles
980 */
981 /**
982 * @section font library path
983 */
984 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
985 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:before,
986 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:after,
987 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:before,
988 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:after,
989 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:before,
990 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:after {
991 content: " ";
992 display: table;
993 }
994 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:after,
995 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:after,
996 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:after {
997 clear: both;
998 }
999 .ui-grid-cell-focus {
1000 outline: 0;
1001 background-color: #b3c4c7;
1002 }
1003 .ui-grid-focuser {
1004 position: absolute;
1005 left: 0px;
1006 top: 0px;
1007 z-index: -1;
1008 width: 100%;
1009 height: 100%;
1010 }
1011 .ui-grid-focuser:focus {
1012 border-color: #66afe9;
1013 outline: 0;
1014 -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
1015 box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
1016 }
1017 .ui-grid-offscreen {
1018 display: block;
1019 position: absolute;
1020 left: -10000px;
1021 top: -10000px;
1022 clip: rect(0px, 0px, 0px, 0px);
1023 }
1024
1025 /* This file contains variable declarations (do not remove this line) */
1026 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1027 /**
1028 * @section Grid styles
1029 */
1030 /**
1031 * @section Header styles
1032 */
1033 /** @description Colors for header gradient */
1034 /**
1035 * @section Grid body styles
1036 */
1037 /** @description Colors used for row alternation */
1038 /**
1039 * @section Sort arrow colors
1040 */
1041 /**
1042 * @section Scrollbar styles
1043 */
1044 /**
1045 * @section font library path
1046 */
1047 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1048 div.ui-grid-cell input {
1049 border-radius: inherit;
1050 padding: 0;
1051 width: 100%;
1052 color: inherit;
1053 height: auto;
1054 font: inherit;
1055 outline: none;
1056 }
1057 div.ui-grid-cell input:focus {
1058 color: inherit;
1059 outline: none;
1060 }
1061 div.ui-grid-cell input[type="checkbox"] {
1062 margin: 9px 0 0 6px;
1063 width: auto;
1064 }
1065 div.ui-grid-cell input.ng-invalid {
1066 border: 1px solid #fc8f8f;
1067 }
1068 div.ui-grid-cell input.ng-valid {
1069 border: 1px solid #d4d4d4;
1070 }
1071
1072 /* This file contains variable declarations (do not remove this line) */
1073 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1074 /**
1075 * @section Grid styles
1076 */
1077 /**
1078 * @section Header styles
1079 */
1080 /** @description Colors for header gradient */
1081 /**
1082 * @section Grid body styles
1083 */
1084 /** @description Colors used for row alternation */
1085 /**
1086 * @section Sort arrow colors
1087 */
1088 /**
1089 * @section Scrollbar styles
1090 */
1091 /**
1092 * @section font library path
1093 */
1094 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1095 .expandableRow .ui-grid-row:nth-child(odd) .ui-grid-cell {
1096 background-color: #fdfdfd;
1097 }
1098 .expandableRow .ui-grid-row:nth-child(even) .ui-grid-cell {
1099 background-color: #f3f3f3;
1100 }
1101 .ui-grid-cell.ui-grid-disable-selection.ui-grid-row-header-cell {
1102 pointer-events: none;
1103 }
1104 .ui-grid-expandable-buttons-cell i {
1105 pointer-events: all;
1106 }
1107 .scrollFiller {
1108 float: left;
1109 border: 1px solid #d4d4d4;
1110 }
1111
1112 /* This file contains variable declarations (do not remove this line) */
1113 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1114 /**
1115 * @section Grid styles
1116 */
1117 /**
1118 * @section Header styles
1119 */
1120 /** @description Colors for header gradient */
1121 /**
1122 * @section Grid body styles
1123 */
1124 /** @description Colors used for row alternation */
1125 /**
1126 * @section Sort arrow colors
1127 */
1128 /**
1129 * @section Scrollbar styles
1130 */
1131 /**
1132 * @section font library path
1133 */
1134 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1135
1136 /* This file contains variable declarations (do not remove this line) */
1137 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1138 /**
1139 * @section Grid styles
1140 */
1141 /**
1142 * @section Header styles
1143 */
1144 /** @description Colors for header gradient */
1145 /**
1146 * @section Grid body styles
1147 */
1148 /** @description Colors used for row alternation */
1149 /**
1150 * @section Sort arrow colors
1151 */
1152 /**
1153 * @section Scrollbar styles
1154 */
1155 /**
1156 * @section font library path
1157 */
1158 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1159 .ui-grid-tree-header-row {
1160 font-weight: bold !important;
1161 }
1162
1163 /* This file contains variable declarations (do not remove this line) */
1164 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1165 /**
1166 * @section Grid styles
1167 */
1168 /**
1169 * @section Header styles
1170 */
1171 /** @description Colors for header gradient */
1172 /**
1173 * @section Grid body styles
1174 */
1175 /** @description Colors used for row alternation */
1176 /**
1177 * @section Sort arrow colors
1178 */
1179 /**
1180 * @section Scrollbar styles
1181 */
1182 /**
1183 * @section font library path
1184 */
1185 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1186
1187 /* This file contains variable declarations (do not remove this line) */
1188 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1189 /**
1190 * @section Grid styles
1191 */
1192 /**
1193 * @section Header styles
1194 */
1195 /** @description Colors for header gradient */
1196 /**
1197 * @section Grid body styles
1198 */
1199 /** @description Colors used for row alternation */
1200 /**
1201 * @section Sort arrow colors
1202 */
1203 /**
1204 * @section Scrollbar styles
1205 */
1206 /**
1207 * @section font library path
1208 */
1209 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1210 .movingColumn {
1211 position: absolute;
1212 top: 0;
1213 border: 1px solid #d4d4d4;
1214 box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2);
1215 }
1216 .movingColumn .ui-grid-icon-angle-down {
1217 display: none;
1218 }
1219
1220 /* This file contains variable declarations (do not remove this line) */
1221 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1222 /**
1223 * @section Grid styles
1224 */
1225 /**
1226 * @section Header styles
1227 */
1228 /** @description Colors for header gradient */
1229 /**
1230 * @section Grid body styles
1231 */
1232 /** @description Colors used for row alternation */
1233 /**
1234 * @section Sort arrow colors
1235 */
1236 /**
1237 * @section Scrollbar styles
1238 */
1239 /**
1240 * @section font library path
1241 */
1242 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1243 /*---------------------------------------------------
1244 LESS Elements 0.9
1245 ---------------------------------------------------
1246 A set of useful LESS mixins
1247 More info at: http://lesselements.com
1248 ---------------------------------------------------*/
1249 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:before,
1250 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:after,
1251 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:before,
1252 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:after,
1253 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:before,
1254 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:after {
1255 content: " ";
1256 display: table;
1257 }
1258 #ui-grid-twbs #ui-grid-twbs .form-horizontal .form-group:after,
1259 #ui-grid-twbs #ui-grid-twbs .btn-toolbar:after,
1260 #ui-grid-twbs #ui-grid-twbs .btn-group-vertical > .btn-group:after {
1261 clear: both;
1262 }
1263 .ui-grid-pager-panel {
1264 position: absolute;
1265 left: 0;
1266 bottom: 0;
1267 width: 100%;
1268 padding-top: 3px;
1269 padding-bottom: 3px;
1270 }
1271 .ui-grid-pager-container {
1272 float: left;
1273 }
1274 .ui-grid-pager-control {
1275 margin-right: 10px;
1276 margin-left: 10px;
1277 min-width: 135px;
1278 float: left;
1279 }
1280 .ui-grid-pager-control button {
1281 height: 25px;
1282 min-width: 26px;
1283 display: inline-block;
1284 margin-bottom: 0;
1285 font-weight: normal;
1286 text-align: center;
1287 vertical-align: middle;
1288 touch-action: manipulation;
1289 cursor: pointer;
1290 background-image: none;
1291 border: 1px solid transparent;
1292 white-space: nowrap;
1293 padding: 6px 12px;
1294 font-size: 14px;
1295 line-height: 1.42857143;
1296 border-radius: 4px;
1297 -webkit-user-select: none;
1298 -moz-user-select: none;
1299 -ms-user-select: none;
1300 user-select: none;
1301 color: #eeeeee;
1302 background-color: #f3f3f3;
1303 border-color: #cccccc;
1304 }
1305 .ui-grid-pager-control button:focus,
1306 .ui-grid-pager-control button:active:focus,
1307 .ui-grid-pager-control button.active:focus,
1308 .ui-grid-pager-control button.focus,
1309 .ui-grid-pager-control button:active.focus,
1310 .ui-grid-pager-control button.active.focus {
1311 outline: thin dotted;
1312 outline: 5px auto -webkit-focus-ring-color;
1313 outline-offset: -2px;
1314 }
1315 .ui-grid-pager-control button:hover,
1316 .ui-grid-pager-control button:focus,
1317 .ui-grid-pager-control button.focus {
1318 color: #333333;
1319 text-decoration: none;
1320 }
1321 .ui-grid-pager-control button:active,
1322 .ui-grid-pager-control button.active {
1323 outline: 0;
1324 background-image: none;
1325 -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
1326 box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
1327 }
1328 .ui-grid-pager-control button.disabled,
1329 .ui-grid-pager-control button[disabled],
1330 fieldset[disabled] .ui-grid-pager-control button {
1331 cursor: not-allowed;
1332 opacity: 0.65;
1333 filter: alpha(opacity=65);
1334 -webkit-box-shadow: none;
1335 box-shadow: none;
1336 }
1337 a.ui-grid-pager-control button.disabled,
1338 fieldset[disabled] a.ui-grid-pager-control button {
1339 pointer-events: none;
1340 }
1341 .ui-grid-pager-control button:focus,
1342 .ui-grid-pager-control button.focus {
1343 color: #eeeeee;
1344 background-color: #dadada;
1345 border-color: #8c8c8c;
1346 }
1347 .ui-grid-pager-control button:hover {
1348 color: #eeeeee;
1349 background-color: #dadada;
1350 border-color: #adadad;
1351 }
1352 .ui-grid-pager-control button:active,
1353 .ui-grid-pager-control button.active,
1354 .open > .dropdown-toggle.ui-grid-pager-control button {
1355 color: #eeeeee;
1356 background-color: #dadada;
1357 border-color: #adadad;
1358 }
1359 .ui-grid-pager-control button:active:hover,
1360 .ui-grid-pager-control button.active:hover,
1361 .open > .dropdown-toggle.ui-grid-pager-control button:hover,
1362 .ui-grid-pager-control button:active:focus,
1363 .ui-grid-pager-control button.active:focus,
1364 .open > .dropdown-toggle.ui-grid-pager-control button:focus,
1365 .ui-grid-pager-control button:active.focus,
1366 .ui-grid-pager-control button.active.focus,
1367 .open > .dropdown-toggle.ui-grid-pager-control button.focus {
1368 color: #eeeeee;
1369 background-color: #c8c8c8;
1370 border-color: #8c8c8c;
1371 }
1372 .ui-grid-pager-control button:active,
1373 .ui-grid-pager-control button.active,
1374 .open > .dropdown-toggle.ui-grid-pager-control button {
1375 background-image: none;
1376 }
1377 .ui-grid-pager-control button.disabled,
1378 .ui-grid-pager-control button[disabled],
1379 fieldset[disabled] .ui-grid-pager-control button,
1380 .ui-grid-pager-control button.disabled:hover,
1381 .ui-grid-pager-control button[disabled]:hover,
1382 fieldset[disabled] .ui-grid-pager-control button:hover,
1383 .ui-grid-pager-control button.disabled:focus,
1384 .ui-grid-pager-control button[disabled]:focus,
1385 fieldset[disabled] .ui-grid-pager-control button:focus,
1386 .ui-grid-pager-control button.disabled.focus,
1387 .ui-grid-pager-control button[disabled].focus,
1388 fieldset[disabled] .ui-grid-pager-control button.focus,
1389 .ui-grid-pager-control button.disabled:active,
1390 .ui-grid-pager-control button[disabled]:active,
1391 fieldset[disabled] .ui-grid-pager-control button:active,
1392 .ui-grid-pager-control button.disabled.active,
1393 .ui-grid-pager-control button[disabled].active,
1394 fieldset[disabled] .ui-grid-pager-control button.active {
1395 background-color: #f3f3f3;
1396 border-color: #cccccc;
1397 }
1398 .ui-grid-pager-control button .badge {
1399 color: #f3f3f3;
1400 background-color: #eeeeee;
1401 }
1402 .ui-grid-pager-control input {
1403 display: block;
1404 width: 100%;
1405 height: 34px;
1406 padding: 6px 12px;
1407 font-size: 14px;
1408 line-height: 1.42857143;
1409 color: #555555;
1410 background-color: #ffffff;
1411 background-image: none;
1412 border: 1px solid #cccccc;
1413 border-radius: 4px;
1414 -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
1415 box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
1416 -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
1417 -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
1418 transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
1419 height: 30px;
1420 padding: 5px 10px;
1421 font-size: 12px;
1422 line-height: 1.5;
1423 border-radius: 3px;
1424 display: inline;
1425 height: 26px;
1426 width: 50px;
1427 vertical-align: top;
1428 }
1429 .ui-grid-pager-control input:focus {
1430 border-color: #66afe9;
1431 outline: 0;
1432 -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
1433 box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
1434 }
1435 .ui-grid-pager-control input::-moz-placeholder {
1436 color: #999999;
1437 opacity: 1;
1438 }
1439 .ui-grid-pager-control input:-ms-input-placeholder {
1440 color: #999999;
1441 }
1442 .ui-grid-pager-control input::-webkit-input-placeholder {
1443 color: #999999;
1444 }
1445 .ui-grid-pager-control input[disabled],
1446 .ui-grid-pager-control input[readonly],
1447 fieldset[disabled] .ui-grid-pager-control input {
1448 background-color: #eeeeee;
1449 opacity: 1;
1450 }
1451 .ui-grid-pager-control input[disabled],
1452 fieldset[disabled] .ui-grid-pager-control input {
1453 cursor: not-allowed;
1454 }
1455 textarea.ui-grid-pager-control input {
1456 height: auto;
1457 }
1458 select.ui-grid-pager-control input {
1459 height: 30px;
1460 line-height: 30px;
1461 }
1462 textarea.ui-grid-pager-control input,
1463 select[multiple].ui-grid-pager-control input {
1464 height: auto;
1465 }
1466 .ui-grid-pager-control .ui-grid-pager-max-pages-number {
1467 vertical-align: bottom;
1468 }
1469 .ui-grid-pager-control .ui-grid-pager-max-pages-number > * {
1470 vertical-align: middle;
1471 }
1472 .ui-grid-pager-control .first-bar {
1473 width: 10px;
1474 border-left: 2px solid #4d4d4d;
1475 margin-top: -6px;
1476 height: 12px;
1477 margin-left: -3px;
1478 }
1479 .ui-grid-pager-control .first-triangle {
1480 width: 0;
1481 height: 0;
1482 border-style: solid;
1483 border-width: 5px 8.7px 5px 0;
1484 border-color: transparent #4d4d4d transparent transparent;
1485 margin-left: 2px;
1486 }
1487 .ui-grid-pager-control .next-triangle {
1488 margin-left: 1px;
1489 }
1490 .ui-grid-pager-control .prev-triangle {
1491 margin-left: 0;
1492 }
1493 .ui-grid-pager-control .last-triangle {
1494 width: 0;
1495 height: 0;
1496 border-style: solid;
1497 border-width: 5px 0 5px 8.7px;
1498 border-color: transparent transparent transparent #4d4d4d;
1499 margin-left: -1px;
1500 }
1501 .ui-grid-pager-control .last-bar {
1502 width: 10px;
1503 border-left: 2px solid #4d4d4d;
1504 margin-top: -6px;
1505 height: 12px;
1506 margin-left: 1px;
1507 }
1508 .ui-grid-pager-row-count-picker {
1509 float: left;
1510 }
1511 .ui-grid-pager-row-count-picker select {
1512 display: block;
1513 width: 100%;
1514 height: 34px;
1515 padding: 6px 12px;
1516 font-size: 14px;
1517 line-height: 1.42857143;
1518 color: #555555;
1519 background-color: #ffffff;
1520 background-image: none;
1521 border: 1px solid #cccccc;
1522 border-radius: 4px;
1523 -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
1524 box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
1525 -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
1526 -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
1527 transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
1528 height: 30px;
1529 padding: 5px 10px;
1530 font-size: 12px;
1531 line-height: 1.5;
1532 border-radius: 3px;
1533 height: 26px;
1534 width: 67px;
1535 display: inline;
1536 }
1537 .ui-grid-pager-row-count-picker select:focus {
1538 border-color: #66afe9;
1539 outline: 0;
1540 -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
1541 box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
1542 }
1543 .ui-grid-pager-row-count-picker select::-moz-placeholder {
1544 color: #999999;
1545 opacity: 1;
1546 }
1547 .ui-grid-pager-row-count-picker select:-ms-input-placeholder {
1548 color: #999999;
1549 }
1550 .ui-grid-pager-row-count-picker select::-webkit-input-placeholder {
1551 color: #999999;
1552 }
1553 .ui-grid-pager-row-count-picker select[disabled],
1554 .ui-grid-pager-row-count-picker select[readonly],
1555 fieldset[disabled] .ui-grid-pager-row-count-picker select {
1556 background-color: #eeeeee;
1557 opacity: 1;
1558 }
1559 .ui-grid-pager-row-count-picker select[disabled],
1560 fieldset[disabled] .ui-grid-pager-row-count-picker select {
1561 cursor: not-allowed;
1562 }
1563 textarea.ui-grid-pager-row-count-picker select {
1564 height: auto;
1565 }
1566 select.ui-grid-pager-row-count-picker select {
1567 height: 30px;
1568 line-height: 30px;
1569 }
1570 textarea.ui-grid-pager-row-count-picker select,
1571 select[multiple].ui-grid-pager-row-count-picker select {
1572 height: auto;
1573 }
1574 .ui-grid-pager-row-count-picker .ui-grid-pager-row-count-label {
1575 margin-top: 3px;
1576 }
1577 .ui-grid-pager-count-container {
1578 float: right;
1579 margin-top: 4px;
1580 min-width: 50px;
1581 }
1582 .ui-grid-pager-count-container .ui-grid-pager-count {
1583 margin-right: 10px;
1584 margin-left: 10px;
1585 float: right;
1586 }
1587
1588 /* This file contains variable declarations (do not remove this line) */
1589 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1590 /**
1591 * @section Grid styles
1592 */
1593 /**
1594 * @section Header styles
1595 */
1596 /** @description Colors for header gradient */
1597 /**
1598 * @section Grid body styles
1599 */
1600 /** @description Colors used for row alternation */
1601 /**
1602 * @section Sort arrow colors
1603 */
1604 /**
1605 * @section Scrollbar styles
1606 */
1607 /**
1608 * @section font library path
1609 */
1610 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1611 .ui-grid-pinned-container {
1612 position: absolute;
1613 display: inline;
1614 top: 0;
1615 }
1616 .ui-grid-pinned-container.ui-grid-pinned-container-left {
1617 float: left;
1618 left: 0;
1619 }
1620 .ui-grid-pinned-container.ui-grid-pinned-container-right {
1621 float: right;
1622 right: 0;
1623 }
1624 .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-header-cell:last-child {
1625 box-sizing: border-box;
1626 border-right: 1px solid;
1627 border-width: 1px;
1628 border-right-color: #aeaeae;
1629 }
1630 .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-cell:last-child {
1631 box-sizing: border-box;
1632 }
1633 .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-header-cell:not(:last-child) .ui-grid-vertical-bar,
1634 .ui-grid-pinned-container .ui-grid-cell:not(:last-child) .ui-grid-vertical-bar {
1635 width: 1px;
1636 }
1637 .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-header-cell:not(:last-child) .ui-grid-vertical-bar {
1638 background-color: #d4d4d4;
1639 }
1640 .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-cell:not(:last-child) .ui-grid-vertical-bar {
1641 background-color: #aeaeae;
1642 }
1643 .ui-grid-pinned-container.ui-grid-pinned-container-left .ui-grid-header-cell:last-child .ui-grid-vertical-bar {
1644 right: -1px;
1645 width: 1px;
1646 background-color: #aeaeae;
1647 }
1648 .ui-grid-pinned-container.ui-grid-pinned-container-right .ui-grid-header-cell:first-child {
1649 box-sizing: border-box;
1650 border-left: 1px solid;
1651 border-width: 1px;
1652 border-left-color: #aeaeae;
1653 }
1654 .ui-grid-pinned-container.ui-grid-pinned-container-right .ui-grid-cell:first-child {
1655 box-sizing: border-box;
1656 border-left: 1px solid;
1657 border-width: 1px;
1658 border-left-color: #aeaeae;
1659 }
1660 .ui-grid-pinned-container.ui-grid-pinned-container-right .ui-grid-header-cell:not(:first-child) .ui-grid-vertical-bar,
1661 .ui-grid-pinned-container .ui-grid-cell:not(:first-child) .ui-grid-vertical-bar {
1662 width: 1px;
1663 }
1664 .ui-grid-pinned-container.ui-grid-pinned-container-right .ui-grid-header-cell:not(:first-child) .ui-grid-vertical-bar {
1665 background-color: #d4d4d4;
1666 }
1667 .ui-grid-pinned-container.ui-grid-pinned-container-right .ui-grid-cell:not(:last-child) .ui-grid-vertical-bar {
1668 background-color: #aeaeae;
1669 }
1670 .ui-grid-pinned-container.ui-grid-pinned-container-first .ui-grid-header-cell:first-child .ui-grid-vertical-bar {
1671 left: -1px;
1672 width: 1px;
1673 background-color: #aeaeae;
1674 }
1675
1676 /* This file contains variable declarations (do not remove this line) */
1677 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1678 /**
1679 * @section Grid styles
1680 */
1681 /**
1682 * @section Header styles
1683 */
1684 /** @description Colors for header gradient */
1685 /**
1686 * @section Grid body styles
1687 */
1688 /** @description Colors used for row alternation */
1689 /**
1690 * @section Sort arrow colors
1691 */
1692 /**
1693 * @section Scrollbar styles
1694 */
1695 /**
1696 * @section font library path
1697 */
1698 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1699 .ui-grid-column-resizer {
1700 top: 0;
1701 bottom: 0;
1702 width: 5px;
1703 position: absolute;
1704 cursor: col-resize;
1705 }
1706 .ui-grid-column-resizer.left {
1707 left: 0;
1708 }
1709 .ui-grid-column-resizer.right {
1710 right: 0;
1711 }
1712 .ui-grid-header-cell:last-child .ui-grid-column-resizer.right {
1713 border-right: 1px solid #d4d4d4;
1714 }
1715 .ui-grid[dir=rtl] .ui-grid-header-cell:last-child .ui-grid-column-resizer.right {
1716 border-right: 0;
1717 }
1718 .ui-grid[dir=rtl] .ui-grid-header-cell:last-child .ui-grid-column-resizer.left {
1719 border-left: 1px solid #d4d4d4;
1720 }
1721 .ui-grid.column-resizing {
1722 cursor: col-resize;
1723 }
1724 .ui-grid.column-resizing .ui-grid-resize-overlay {
1725 position: absolute;
1726 top: 0;
1727 height: 100%;
1728 width: 1px;
1729 background-color: #aeaeae;
1730 }
1731
1732 /* This file contains variable declarations (do not remove this line) */
1733 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1734 /**
1735 * @section Grid styles
1736 */
1737 /**
1738 * @section Header styles
1739 */
1740 /** @description Colors for header gradient */
1741 /**
1742 * @section Grid body styles
1743 */
1744 /** @description Colors used for row alternation */
1745 /**
1746 * @section Sort arrow colors
1747 */
1748 /**
1749 * @section Scrollbar styles
1750 */
1751 /**
1752 * @section font library path
1753 */
1754 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1755 .ui-grid-row-saving .ui-grid-cell {
1756 color: #848484 !important;
1757 }
1758 .ui-grid-row-dirty .ui-grid-cell {
1759 color: #610b38;
1760 }
1761 .ui-grid-row-error .ui-grid-cell {
1762 color: #ff0000 !important;
1763 }
1764
1765 /* This file contains variable declarations (do not remove this line) */
1766 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1767 /**
1768 * @section Grid styles
1769 */
1770 /**
1771 * @section Header styles
1772 */
1773 /** @description Colors for header gradient */
1774 /**
1775 * @section Grid body styles
1776 */
1777 /** @description Colors used for row alternation */
1778 /**
1779 * @section Sort arrow colors
1780 */
1781 /**
1782 * @section Scrollbar styles
1783 */
1784 /**
1785 * @section font library path
1786 */
1787 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1788 .ui-grid-row.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell {
1789 background-color: #f2dede;
1790 }
1791 .ui-grid-selection-row-header-buttons {
1792 cursor: pointer;
1793 opacity: 0.1;
1794 }
1795 .ui-grid-selection-row-header-buttons.ui-grid-row-selected {
1796 opacity: 1;
1797 }
1798 .ui-grid-selection-row-header-buttons.ui-grid-all-selected {
1799 opacity: 1;
1800 }
1801
1802 /* This file contains variable declarations (do not remove this line) */
1803 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1804 /**
1805 * @section Grid styles
1806 */
1807 /**
1808 * @section Header styles
1809 */
1810 /** @description Colors for header gradient */
1811 /**
1812 * @section Grid body styles
1813 */
1814 /** @description Colors used for row alternation */
1815 /**
1816 * @section Sort arrow colors
1817 */
1818 /**
1819 * @section Scrollbar styles
1820 */
1821 /**
1822 * @section font library path
1823 */
1824 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1825 .ui-grid-tree-row-header-buttons.ui-grid-tree-header {
1826 cursor: pointer;
1827 opacity: 1;
1828 }
1829
1830 /* This file contains variable declarations (do not remove this line) */
1831 /*-- VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1832 /**
1833 * @section Grid styles
1834 */
1835 /**
1836 * @section Header styles
1837 */
1838 /** @description Colors for header gradient */
1839 /**
1840 * @section Grid body styles
1841 */
1842 /** @description Colors used for row alternation */
1843 /**
1844 * @section Sort arrow colors
1845 */
1846 /**
1847 * @section Scrollbar styles
1848 */
1849 /**
1850 * @section font library path
1851 */
1852 /*-- END VARIABLES (DO NOT REMOVE THESE COMMENTS) --*/
1853 .ui-grid-tree-header-row {
1854 font-weight: bold !important;
1855 }
1856
1857 .ui-grid-icon-plus-squared:before {
1858 content: '\c350';
1859 }
1860 /* '썐' */
1861 .ui-grid-icon-minus-squared:before {
1862 content: '\c351';
1863 }
1864 /* '썑' */
1865 .ui-grid-icon-search:before {
1866 content: '\c352';
1867 }
1868 /* '썒' */
1869 .ui-grid-icon-cancel:before {
1870 content: '\c353';
1871 }
1872 /* '썓' */
1873 .ui-grid-icon-info-circled:before {
1874 content: '\c354';
1875 }
1876 /* '썔' */
1877 .ui-grid-icon-lock:before {
1878 content: '\c355';
1879 }
1880 /* '썕' */
1881 .ui-grid-icon-lock-open:before {
1882 content: '\c356';
1883 }
1884 /* '썖' */
1885 .ui-grid-icon-pencil:before {
1886 content: '\c357';
1887 }
1888 /* '썗' */
1889 .ui-grid-icon-down-dir:before {
1890 content: '\c358';
1891 }
1892 /* '썘' */
1893 .ui-grid-icon-up-dir:before {
1894 content: '\c359';
1895 }
1896 /* '썙' */
1897 .ui-grid-icon-left-dir:before {
1898 content: '\c35a';
1899 }
1900 /* '썚' */
1901 .ui-grid-icon-right-dir:before {
1902 content: '\c35b';
1903 }
1904 /* '썛' */
1905 .ui-grid-icon-left-open:before {
1906 content: '\c35c';
1907 }
1908 /* '썜' */
1909 .ui-grid-icon-right-open:before {
1910 content: '\c35d';
1911 }
1912 /* '썝' */
1913 .ui-grid-icon-angle-down:before {
1914 content: '\c35e';
1915 }
1916 /* '썞' */
1917 .ui-grid-icon-filter:before {
1918 content: '\c35f';
1919 }
1920 /* '썟' */
1921 .ui-grid-icon-sort-alt-up:before {
1922 content: '\c360';
1923 }
1924 /* 'ì ' */
1925 .ui-grid-icon-sort-alt-down:before {
1926 content: '\c361';
1927 }
1928 /* '썡' */
1929 .ui-grid-icon-ok:before {
1930 content: '\c362';
1931 }
1932 /* '썢' */
1933 .ui-grid-icon-menu:before {
1934 content: '\c363';
1935 }
1936 /* '썣' */
1937 .ui-grid-icon-indent-left:before {
1938 content: '\e800';
1939 }
1940 /* 'î €' */
1941 .ui-grid-icon-indent-right:before {
1942 content: '\e801';
1943 }
1944 /* 'î ' */
1945 .ui-grid-icon-spin5:before {
1946 content: '\ea61';
1947 }
1948 /* 'î©¡' */
1949 .crop-text {
1950 overflow: hidden;
1951 -o-text-overflow: ellipsis;
1952 -ms-text-overflow: ellipsis;
1953 text-overflow: ellipsis;
1954 white-space: nowrap;
1955 }
1956 .ui-grid .ui-grid-row:nth-child(odd) .ui-grid-cell .ui-grid-cell-contents .pos-middle,
1957 .ui-grid .ui-grid-row:nth-child(even) .ui-grid-cell .ui-grid-cell-contents .pos-middle {
1958 position: relative;
1959 top: 50%;
1960 -webkit-transform: translateY(-50%);
1961 -ms-transform: translateY(-50%);
1962 -moz-transform: translateY(-50%);
1963 -o-transform: translateY(-50%);
1964 transform: translateY(-50%);
1965 }
1966 .ui-grid .ui-grid-row:nth-child(odd) .ui-grid-cell .ui-grid-cell-contents .crop-text,
1967 .ui-grid .ui-grid-row:nth-child(even) .ui-grid-cell .ui-grid-cell-contents .crop-text {
1968 overflow: hidden;
1969 -o-text-overflow: ellipsis;
1970 -ms-text-overflow: ellipsis;
1971 text-overflow: ellipsis;
1972 white-space: nowrap;
1973 }
0 /*!
1 * ui-grid - v3.0.7 - 2015-10-06
2 * Copyright (c) 2015 ; License: MIT
3 */
4
5 (function () {
6 'use strict';
7 angular.module('ui.grid.i18n', []);
8 angular.module('ui.grid', ['ui.grid.i18n']);
9 })();
10 (function () {
11 'use strict';
12 angular.module('ui.grid').constant('uiGridConstants', {
13 LOG_DEBUG_MESSAGES: true,
14 LOG_WARN_MESSAGES: true,
15 LOG_ERROR_MESSAGES: true,
16 CUSTOM_FILTERS: /CUSTOM_FILTERS/g,
17 COL_FIELD: /COL_FIELD/g,
18 MODEL_COL_FIELD: /MODEL_COL_FIELD/g,
19 TOOLTIP: /title=\"TOOLTIP\"/g,
20 DISPLAY_CELL_TEMPLATE: /DISPLAY_CELL_TEMPLATE/g,
21 TEMPLATE_REGEXP: /<.+>/,
22 FUNC_REGEXP: /(\([^)]*\))?$/,
23 DOT_REGEXP: /\./g,
24 APOS_REGEXP: /'/g,
25 BRACKET_REGEXP: /^(.*)((?:\s*\[\s*\d+\s*\]\s*)|(?:\s*\[\s*"(?:[^"\\]|\\.)*"\s*\]\s*)|(?:\s*\[\s*'(?:[^'\\]|\\.)*'\s*\]\s*))(.*)$/,
26 COL_CLASS_PREFIX: 'ui-grid-col',
27 events: {
28 GRID_SCROLL: 'uiGridScroll',
29 COLUMN_MENU_SHOWN: 'uiGridColMenuShown',
30 ITEM_DRAGGING: 'uiGridItemDragStart', // For any item being dragged
31 COLUMN_HEADER_CLICK: 'uiGridColumnHeaderClick'
32 },
33 // copied from http://www.lsauer.com/2011/08/javascript-keymap-keycodes-in-json.html
34 keymap: {
35 TAB: 9,
36 STRG: 17,
37 CAPSLOCK: 20,
38 CTRL: 17,
39 CTRLRIGHT: 18,
40 CTRLR: 18,
41 SHIFT: 16,
42 RETURN: 13,
43 ENTER: 13,
44 BACKSPACE: 8,
45 BCKSP: 8,
46 ALT: 18,
47 ALTR: 17,
48 ALTRIGHT: 17,
49 SPACE: 32,
50 WIN: 91,
51 MAC: 91,
52 FN: null,
53 PG_UP: 33,
54 PG_DOWN: 34,
55 UP: 38,
56 DOWN: 40,
57 LEFT: 37,
58 RIGHT: 39,
59 ESC: 27,
60 DEL: 46,
61 F1: 112,
62 F2: 113,
63 F3: 114,
64 F4: 115,
65 F5: 116,
66 F6: 117,
67 F7: 118,
68 F8: 119,
69 F9: 120,
70 F10: 121,
71 F11: 122,
72 F12: 123
73 },
74 ASC: 'asc',
75 DESC: 'desc',
76 filter: {
77 STARTS_WITH: 2,
78 ENDS_WITH: 4,
79 EXACT: 8,
80 CONTAINS: 16,
81 GREATER_THAN: 32,
82 GREATER_THAN_OR_EQUAL: 64,
83 LESS_THAN: 128,
84 LESS_THAN_OR_EQUAL: 256,
85 NOT_EQUAL: 512,
86 SELECT: 'select',
87 INPUT: 'input'
88 },
89
90 aggregationTypes: {
91 sum: 2,
92 count: 4,
93 avg: 8,
94 min: 16,
95 max: 32
96 },
97
98 // TODO(c0bra): Create full list of these somehow. NOTE: do any allow a space before or after them?
99 CURRENCY_SYMBOLS: ['ƒ', '$', '£', '$', '¤', '¥', '៛', '₩', '₱', '฿', '₫'],
100
101 scrollDirection: {
102 UP: 'up',
103 DOWN: 'down',
104 LEFT: 'left',
105 RIGHT: 'right',
106 NONE: 'none'
107
108 },
109
110 dataChange: {
111 ALL: 'all',
112 EDIT: 'edit',
113 ROW: 'row',
114 COLUMN: 'column',
115 OPTIONS: 'options'
116 },
117 scrollbars: {
118 NEVER: 0,
119 ALWAYS: 1
120 //WHEN_NEEDED: 2
121 }
122 });
123
124 })();
125 angular.module('ui.grid').directive('uiGridCell', ['$compile', '$parse', 'gridUtil', 'uiGridConstants', function ($compile, $parse, gridUtil, uiGridConstants) {
126 var uiGridCell = {
127 priority: 0,
128 scope: false,
129 require: '?^uiGrid',
130 compile: function() {
131 return {
132 pre: function($scope, $elm, $attrs, uiGridCtrl) {
133 function compileTemplate() {
134 var compiledElementFn = $scope.col.compiledElementFn;
135
136 compiledElementFn($scope, function(clonedElement, scope) {
137 $elm.append(clonedElement);
138 });
139 }
140
141 // If the grid controller is present, use it to get the compiled cell template function
142 if (uiGridCtrl && $scope.col.compiledElementFn) {
143 compileTemplate();
144 }
145 // No controller, compile the element manually (for unit tests)
146 else {
147 if ( uiGridCtrl && !$scope.col.compiledElementFn ){
148 // gridUtil.logError('Render has been called before precompile. Please log a ui-grid issue');
149
150 $scope.col.getCompiledElementFn()
151 .then(function (compiledElementFn) {
152 compiledElementFn($scope, function(clonedElement, scope) {
153 $elm.append(clonedElement);
154 });
155 });
156 }
157 else {
158 var html = $scope.col.cellTemplate
159 .replace(uiGridConstants.MODEL_COL_FIELD, 'row.entity.' + gridUtil.preEval($scope.col.field))
160 .replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
161
162 var cellElement = $compile(html)($scope);
163 $elm.append(cellElement);
164 }
165 }
166 },
167 post: function($scope, $elm, $attrs, uiGridCtrl) {
168 var initColClass = $scope.col.getColClass(false);
169 $elm.addClass(initColClass);
170
171 var classAdded;
172 var updateClass = function( grid ){
173 var contents = $elm;
174 if ( classAdded ){
175 contents.removeClass( classAdded );
176 classAdded = null;
177 }
178
179 if (angular.isFunction($scope.col.cellClass)) {
180 classAdded = $scope.col.cellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
181 }
182 else {
183 classAdded = $scope.col.cellClass;
184 }
185 contents.addClass(classAdded);
186 };
187
188 if ($scope.col.cellClass) {
189 updateClass();
190 }
191
192 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
193 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN, uiGridConstants.dataChange.EDIT]);
194
195 // watch the col and row to see if they change - which would indicate that we've scrolled or sorted or otherwise
196 // changed the row/col that this cell relates to, and we need to re-evaluate cell classes and maybe other things
197 var cellChangeFunction = function( n, o ){
198 if ( n !== o ) {
199 if ( classAdded || $scope.col.cellClass ){
200 updateClass();
201 }
202
203 // See if the column's internal class has changed
204 var newColClass = $scope.col.getColClass(false);
205 if (newColClass !== initColClass) {
206 $elm.removeClass(initColClass);
207 $elm.addClass(newColClass);
208 initColClass = newColClass;
209 }
210 }
211 };
212
213 // TODO(c0bra): Turn this into a deep array watch
214 /* shouldn't be needed any more given track by col.name
215 var colWatchDereg = $scope.$watch( 'col', cellChangeFunction );
216 */
217 var rowWatchDereg = $scope.$watch( 'row', cellChangeFunction );
218
219
220 var deregisterFunction = function() {
221 dataChangeDereg();
222 // colWatchDereg();
223 rowWatchDereg();
224 };
225
226 $scope.$on( '$destroy', deregisterFunction );
227 $elm.on( '$destroy', deregisterFunction );
228 }
229 };
230 }
231 };
232
233 return uiGridCell;
234 }]);
235
236
237 (function(){
238
239 angular.module('ui.grid')
240 .service('uiGridColumnMenuService', [ 'i18nService', 'uiGridConstants', 'gridUtil',
241 function ( i18nService, uiGridConstants, gridUtil ) {
242 /**
243 * @ngdoc service
244 * @name ui.grid.service:uiGridColumnMenuService
245 *
246 * @description Services for working with column menus, factored out
247 * to make the code easier to understand
248 */
249
250 var service = {
251 /**
252 * @ngdoc method
253 * @methodOf ui.grid.service:uiGridColumnMenuService
254 * @name initialize
255 * @description Sets defaults, puts a reference to the $scope on
256 * the uiGridController
257 * @param {$scope} $scope the $scope from the uiGridColumnMenu
258 * @param {controller} uiGridCtrl the uiGridController for the grid
259 * we're on
260 *
261 */
262 initialize: function( $scope, uiGridCtrl ){
263 $scope.grid = uiGridCtrl.grid;
264
265 // Store a reference to this link/controller in the main uiGrid controller
266 // to allow showMenu later
267 uiGridCtrl.columnMenuScope = $scope;
268
269 // Save whether we're shown or not so the columns can check
270 $scope.menuShown = false;
271 },
272
273
274 /**
275 * @ngdoc method
276 * @methodOf ui.grid.service:uiGridColumnMenuService
277 * @name setColMenuItemWatch
278 * @description Setup a watch on $scope.col.menuItems, and update
279 * menuItems based on this. $scope.col needs to be set by the column
280 * before calling the menu.
281 * @param {$scope} $scope the $scope from the uiGridColumnMenu
282 * @param {controller} uiGridCtrl the uiGridController for the grid
283 * we're on
284 *
285 */
286 setColMenuItemWatch: function ( $scope ){
287 var deregFunction = $scope.$watch('col.menuItems', function (n, o) {
288 if (typeof(n) !== 'undefined' && n && angular.isArray(n)) {
289 n.forEach(function (item) {
290 if (typeof(item.context) === 'undefined' || !item.context) {
291 item.context = {};
292 }
293 item.context.col = $scope.col;
294 });
295
296 $scope.menuItems = $scope.defaultMenuItems.concat(n);
297 }
298 else {
299 $scope.menuItems = $scope.defaultMenuItems;
300 }
301 });
302
303 $scope.$on( '$destroy', deregFunction );
304 },
305
306
307 /**
308 * @ngdoc boolean
309 * @name enableSorting
310 * @propertyOf ui.grid.class:GridOptions.columnDef
311 * @description (optional) True by default. When enabled, this setting adds sort
312 * widgets to the column header, allowing sorting of the data in the individual column.
313 */
314 /**
315 * @ngdoc method
316 * @methodOf ui.grid.service:uiGridColumnMenuService
317 * @name sortable
318 * @description determines whether this column is sortable
319 * @param {$scope} $scope the $scope from the uiGridColumnMenu
320 *
321 */
322 sortable: function( $scope ) {
323 if ( $scope.grid.options.enableSorting && typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.enableSorting) {
324 return true;
325 }
326 else {
327 return false;
328 }
329 },
330
331 /**
332 * @ngdoc method
333 * @methodOf ui.grid.service:uiGridColumnMenuService
334 * @name isActiveSort
335 * @description determines whether the requested sort direction is current active, to
336 * allow highlighting in the menu
337 * @param {$scope} $scope the $scope from the uiGridColumnMenu
338 * @param {string} direction the direction that we'd have selected for us to be active
339 *
340 */
341 isActiveSort: function( $scope, direction ){
342 return (typeof($scope.col) !== 'undefined' && typeof($scope.col.sort) !== 'undefined' &&
343 typeof($scope.col.sort.direction) !== 'undefined' && $scope.col.sort.direction === direction);
344
345 },
346
347 /**
348 * @ngdoc method
349 * @methodOf ui.grid.service:uiGridColumnMenuService
350 * @name suppressRemoveSort
351 * @description determines whether we should suppress the removeSort option
352 * @param {$scope} $scope the $scope from the uiGridColumnMenu
353 *
354 */
355 suppressRemoveSort: function( $scope ) {
356 if ($scope.col && $scope.col.suppressRemoveSort) {
357 return true;
358 }
359 else {
360 return false;
361 }
362 },
363
364
365 /**
366 * @ngdoc boolean
367 * @name enableHiding
368 * @propertyOf ui.grid.class:GridOptions.columnDef
369 * @description (optional) True by default. When set to false, this setting prevents a user from hiding the column
370 * using the column menu or the grid menu.
371 */
372 /**
373 * @ngdoc method
374 * @methodOf ui.grid.service:uiGridColumnMenuService
375 * @name hideable
376 * @description determines whether a column can be hidden, by checking the enableHiding columnDef option
377 * @param {$scope} $scope the $scope from the uiGridColumnMenu
378 *
379 */
380 hideable: function( $scope ) {
381 if (typeof($scope.col) !== 'undefined' && $scope.col && $scope.col.colDef && $scope.col.colDef.enableHiding === false ) {
382 return false;
383 }
384 else {
385 return true;
386 }
387 },
388
389
390 /**
391 * @ngdoc method
392 * @methodOf ui.grid.service:uiGridColumnMenuService
393 * @name getDefaultMenuItems
394 * @description returns the default menu items for a column menu
395 * @param {$scope} $scope the $scope from the uiGridColumnMenu
396 *
397 */
398 getDefaultMenuItems: function( $scope ){
399 return [
400 {
401 title: i18nService.getSafeText('sort.ascending'),
402 icon: 'ui-grid-icon-sort-alt-up',
403 action: function($event) {
404 $event.stopPropagation();
405 $scope.sortColumn($event, uiGridConstants.ASC);
406 },
407 shown: function () {
408 return service.sortable( $scope );
409 },
410 active: function() {
411 return service.isActiveSort( $scope, uiGridConstants.ASC);
412 }
413 },
414 {
415 title: i18nService.getSafeText('sort.descending'),
416 icon: 'ui-grid-icon-sort-alt-down',
417 action: function($event) {
418 $event.stopPropagation();
419 $scope.sortColumn($event, uiGridConstants.DESC);
420 },
421 shown: function() {
422 return service.sortable( $scope );
423 },
424 active: function() {
425 return service.isActiveSort( $scope, uiGridConstants.DESC);
426 }
427 },
428 {
429 title: i18nService.getSafeText('sort.remove'),
430 icon: 'ui-grid-icon-cancel',
431 action: function ($event) {
432 $event.stopPropagation();
433 $scope.unsortColumn();
434 },
435 shown: function() {
436 return service.sortable( $scope ) &&
437 typeof($scope.col) !== 'undefined' && (typeof($scope.col.sort) !== 'undefined' &&
438 typeof($scope.col.sort.direction) !== 'undefined') && $scope.col.sort.direction !== null &&
439 !service.suppressRemoveSort( $scope );
440 }
441 },
442 {
443 title: i18nService.getSafeText('column.hide'),
444 icon: 'ui-grid-icon-cancel',
445 shown: function() {
446 return service.hideable( $scope );
447 },
448 action: function ($event) {
449 $event.stopPropagation();
450 $scope.hideColumn();
451 }
452 },
453 {
454 title: i18nService.getSafeText('columnMenu.close'),
455 screenReaderOnly: true,
456 shown: function(){
457 return true;
458 },
459 action: function($event){
460 $event.stopPropagation();
461 }
462 }
463 ];
464 },
465
466
467 /**
468 * @ngdoc method
469 * @methodOf ui.grid.service:uiGridColumnMenuService
470 * @name getColumnElementPosition
471 * @description gets the position information needed to place the column
472 * menu below the column header
473 * @param {$scope} $scope the $scope from the uiGridColumnMenu
474 * @param {GridCol} column the column we want to position below
475 * @param {element} $columnElement the column element we want to position below
476 * @returns {hash} containing left, top, offset, height, width
477 *
478 */
479 getColumnElementPosition: function( $scope, column, $columnElement ){
480 var positionData = {};
481 positionData.left = $columnElement[0].offsetLeft;
482 positionData.top = $columnElement[0].offsetTop;
483 positionData.parentLeft = $columnElement[0].offsetParent.offsetLeft;
484
485 // Get the grid scrollLeft
486 positionData.offset = 0;
487 if (column.grid.options.offsetLeft) {
488 positionData.offset = column.grid.options.offsetLeft;
489 }
490
491 positionData.height = gridUtil.elementHeight($columnElement, true);
492 positionData.width = gridUtil.elementWidth($columnElement, true);
493
494 return positionData;
495 },
496
497
498 /**
499 * @ngdoc method
500 * @methodOf ui.grid.service:uiGridColumnMenuService
501 * @name repositionMenu
502 * @description Reposition the menu below the new column. If the menu has no child nodes
503 * (i.e. it's not currently visible) then we guess it's width at 100, we'll be called again
504 * later to fix it
505 * @param {$scope} $scope the $scope from the uiGridColumnMenu
506 * @param {GridCol} column the column we want to position below
507 * @param {hash} positionData a hash containing left, top, offset, height, width
508 * @param {element} $elm the column menu element that we want to reposition
509 * @param {element} $columnElement the column element that we want to reposition underneath
510 *
511 */
512 repositionMenu: function( $scope, column, positionData, $elm, $columnElement ) {
513 var menu = $elm[0].querySelectorAll('.ui-grid-menu');
514 var containerId = column.renderContainer ? column.renderContainer : 'body';
515 var renderContainer = column.grid.renderContainers[containerId];
516
517 // It's possible that the render container of the column we're attaching to is
518 // offset from the grid (i.e. pinned containers), we need to get the difference in the offsetLeft
519 // between the render container and the grid
520 var renderContainerElm = gridUtil.closestElm($columnElement, '.ui-grid-render-container');
521 var renderContainerOffset = renderContainerElm.getBoundingClientRect().left - $scope.grid.element[0].getBoundingClientRect().left;
522
523 var containerScrollLeft = renderContainerElm.querySelectorAll('.ui-grid-viewport')[0].scrollLeft;
524
525 // default value the last width for _this_ column, otherwise last width for _any_ column, otherwise default to 170
526 var myWidth = column.lastMenuWidth ? column.lastMenuWidth : ( $scope.lastMenuWidth ? $scope.lastMenuWidth : 170);
527 var paddingRight = column.lastMenuPaddingRight ? column.lastMenuPaddingRight : ( $scope.lastMenuPaddingRight ? $scope.lastMenuPaddingRight : 10);
528
529 if ( menu.length !== 0 ){
530 var mid = menu[0].querySelectorAll('.ui-grid-menu-mid');
531 if ( mid.length !== 0 && !angular.element(mid).hasClass('ng-hide') ) {
532 myWidth = gridUtil.elementWidth(menu, true);
533 $scope.lastMenuWidth = myWidth;
534 column.lastMenuWidth = myWidth;
535
536 // TODO(c0bra): use padding-left/padding-right based on document direction (ltr/rtl), place menu on proper side
537 // Get the column menu right padding
538 paddingRight = parseInt(gridUtil.getStyles(angular.element(menu)[0])['paddingRight'], 10);
539 $scope.lastMenuPaddingRight = paddingRight;
540 column.lastMenuPaddingRight = paddingRight;
541 }
542 }
543
544 var left = positionData.left + renderContainerOffset - containerScrollLeft + positionData.parentLeft + positionData.width - myWidth + paddingRight;
545 if (left < positionData.offset){
546 left = positionData.offset;
547 }
548
549 $elm.css('left', left + 'px');
550 $elm.css('top', (positionData.top + positionData.height) + 'px');
551 }
552
553 };
554
555 return service;
556 }])
557
558
559 .directive('uiGridColumnMenu', ['$timeout', 'gridUtil', 'uiGridConstants', 'uiGridColumnMenuService', '$document',
560 function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService, $document) {
561 /**
562 * @ngdoc directive
563 * @name ui.grid.directive:uiGridColumnMenu
564 * @description Provides the column menu framework, leverages uiGridMenu underneath
565 *
566 */
567
568 var uiGridColumnMenu = {
569 priority: 0,
570 scope: true,
571 require: '^uiGrid',
572 templateUrl: 'ui-grid/uiGridColumnMenu',
573 replace: true,
574 link: function ($scope, $elm, $attrs, uiGridCtrl) {
575 var self = this;
576
577 uiGridColumnMenuService.initialize( $scope, uiGridCtrl );
578
579 $scope.defaultMenuItems = uiGridColumnMenuService.getDefaultMenuItems( $scope );
580
581 // Set the menu items for use with the column menu. The user can later add additional items via the watch
582 $scope.menuItems = $scope.defaultMenuItems;
583 uiGridColumnMenuService.setColMenuItemWatch( $scope );
584
585
586 /**
587 * @ngdoc method
588 * @methodOf ui.grid.directive:uiGridColumnMenu
589 * @name showMenu
590 * @description Shows the column menu. If the menu is already displayed it
591 * calls the menu to ask it to hide (it will animate), then it repositions the menu
592 * to the right place whilst hidden (it will make an assumption on menu width),
593 * then it asks the menu to show (it will animate), then it repositions the menu again
594 * once we can calculate it's size.
595 * @param {GridCol} column the column we want to position below
596 * @param {element} $columnElement the column element we want to position below
597 */
598 $scope.showMenu = function(column, $columnElement, event) {
599 // Swap to this column
600 $scope.col = column;
601
602 // Get the position information for the column element
603 var colElementPosition = uiGridColumnMenuService.getColumnElementPosition( $scope, column, $columnElement );
604
605 if ($scope.menuShown) {
606 // we want to hide, then reposition, then show, but we want to wait for animations
607 // we set a variable, and then rely on the menu-hidden event to call the reposition and show
608 $scope.colElement = $columnElement;
609 $scope.colElementPosition = colElementPosition;
610 $scope.hideThenShow = true;
611
612 $scope.$broadcast('hide-menu', { originalEvent: event });
613 } else {
614 self.shown = $scope.menuShown = true;
615 uiGridColumnMenuService.repositionMenu( $scope, column, colElementPosition, $elm, $columnElement );
616
617 $scope.colElement = $columnElement;
618 $scope.colElementPosition = colElementPosition;
619 $scope.$broadcast('show-menu', { originalEvent: event });
620 }
621 };
622
623
624 /**
625 * @ngdoc method
626 * @methodOf ui.grid.directive:uiGridColumnMenu
627 * @name hideMenu
628 * @description Hides the column menu.
629 * @param {boolean} broadcastTrigger true if we were triggered by a broadcast
630 * from the menu itself - in which case don't broadcast again as we'll get
631 * an infinite loop
632 */
633 $scope.hideMenu = function( broadcastTrigger ) {
634 $scope.menuShown = false;
635 if ( !broadcastTrigger ){
636 $scope.$broadcast('hide-menu');
637 }
638 };
639
640
641 $scope.$on('menu-hidden', function() {
642 if ( $scope.hideThenShow ){
643 delete $scope.hideThenShow;
644
645 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
646 $scope.$broadcast('show-menu');
647
648 $scope.menuShown = true;
649 } else {
650 $scope.hideMenu( true );
651
652 if ($scope.col) {
653 //Focus on the menu button
654 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + $scope.col.getColClass()+ ' .ui-grid-column-menu-button', $scope.col.grid, false);
655 }
656 }
657 });
658
659 $scope.$on('menu-shown', function() {
660 $timeout( function() {
661 uiGridColumnMenuService.repositionMenu( $scope, $scope.col, $scope.colElementPosition, $elm, $scope.colElement );
662 delete $scope.colElementPosition;
663 delete $scope.columnElement;
664 }, 200);
665 });
666
667
668 /* Column methods */
669 $scope.sortColumn = function (event, dir) {
670 event.stopPropagation();
671
672 $scope.grid.sortColumn($scope.col, dir, true)
673 .then(function () {
674 $scope.grid.refresh();
675 $scope.hideMenu();
676 });
677 };
678
679 $scope.unsortColumn = function () {
680 $scope.col.unsort();
681
682 $scope.grid.refresh();
683 $scope.hideMenu();
684 };
685
686 //Since we are hiding this column the default hide action will fail so we need to focus somewhere else.
687 var setFocusOnHideColumn = function(){
688 $timeout(function(){
689 // Get the UID of the first
690 var focusToGridMenu = function(){
691 return gridUtil.focus.byId('grid-menu', $scope.grid);
692 };
693
694 var thisIndex;
695 $scope.grid.columns.some(function(element, index){
696 if (angular.equals(element, $scope.col)) {
697 thisIndex = index;
698 return true;
699 }
700 });
701
702 var previousVisibleCol;
703 // Try and find the next lower or nearest column to focus on
704 $scope.grid.columns.some(function(element, index){
705 if (!element.visible){
706 return false;
707 } // This columns index is below the current column index
708 else if ( index < thisIndex){
709 previousVisibleCol = element;
710 } // This elements index is above this column index and we haven't found one that is lower
711 else if ( index > thisIndex && !previousVisibleCol) {
712 // This is the next best thing
713 previousVisibleCol = element;
714 // We've found one so use it.
715 return true;
716 } // We've reached an element with an index above this column and the previousVisibleCol variable has been set
717 else if (index > thisIndex && previousVisibleCol) {
718 // We are done.
719 return true;
720 }
721 });
722 // If found then focus on it
723 if (previousVisibleCol){
724 var colClass = previousVisibleCol.getColClass();
725 gridUtil.focus.bySelector($document, '.ui-grid-header-cell.' + colClass+ ' .ui-grid-header-cell-primary-focus', true).then(angular.noop, function(reason){
726 if (reason !== 'canceled'){ // If this is canceled then don't perform the action
727 //The fallback action is to focus on the grid menu
728 return focusToGridMenu();
729 }
730 });
731 } else {
732 // Fallback action to focus on the grid menu
733 focusToGridMenu();
734 }
735 });
736 };
737
738 $scope.hideColumn = function () {
739 $scope.col.colDef.visible = false;
740 $scope.col.visible = false;
741
742 $scope.grid.queueGridRefresh();
743 $scope.hideMenu();
744 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
745 $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col );
746
747 // We are hiding so the default action of focusing on the button that opened this menu will fail.
748 setFocusOnHideColumn();
749 };
750 },
751
752
753
754 controller: ['$scope', function ($scope) {
755 var self = this;
756
757 $scope.$watch('menuItems', function (n, o) {
758 self.menuItems = n;
759 });
760 }]
761 };
762
763 return uiGridColumnMenu;
764
765 }]);
766
767 })();
768
769 (function(){
770 'use strict';
771
772 angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', 'i18nService', 'gridUtil', function ($compile, $templateCache, i18nService, gridUtil) {
773
774 return {
775 compile: function() {
776 return {
777 pre: function ($scope, $elm, $attrs, controllers) {
778 $scope.col.updateFilters = function( filterable ){
779 $elm.children().remove();
780 if ( filterable ){
781 var template = $scope.col.filterHeaderTemplate;
782
783 $elm.append($compile(template)($scope));
784 }
785 };
786
787 $scope.$on( '$destroy', function() {
788 delete $scope.col.updateFilters;
789 });
790 },
791 post: function ($scope, $elm, $attrs, controllers){
792 $scope.aria = i18nService.getSafeText('headerCell.aria');
793 $scope.removeFilter = function(colFilter, index){
794 colFilter.term = null;
795 //Set the focus to the filter input after the action disables the button
796 gridUtil.focus.bySelector($elm, '.ui-grid-filter-input-' + index);
797 };
798 }
799 };
800 }
801 };
802 }]);
803 })();
804
805 (function () {
806 'use strict';
807
808 angular.module('ui.grid').directive('uiGridFooterCell', ['$timeout', 'gridUtil', 'uiGridConstants', '$compile',
809 function ($timeout, gridUtil, uiGridConstants, $compile) {
810 var uiGridFooterCell = {
811 priority: 0,
812 scope: {
813 col: '=',
814 row: '=',
815 renderIndex: '='
816 },
817 replace: true,
818 require: '^uiGrid',
819 compile: function compile(tElement, tAttrs, transclude) {
820 return {
821 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
822 var cellFooter = $compile($scope.col.footerCellTemplate)($scope);
823 $elm.append(cellFooter);
824 },
825 post: function ($scope, $elm, $attrs, uiGridCtrl) {
826 //$elm.addClass($scope.col.getColClass(false));
827 $scope.grid = uiGridCtrl.grid;
828
829 var initColClass = $scope.col.getColClass(false);
830 $elm.addClass(initColClass);
831
832 // apply any footerCellClass
833 var classAdded;
834 var updateClass = function( grid ){
835 var contents = $elm;
836 if ( classAdded ){
837 contents.removeClass( classAdded );
838 classAdded = null;
839 }
840
841 if (angular.isFunction($scope.col.footerCellClass)) {
842 classAdded = $scope.col.footerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
843 }
844 else {
845 classAdded = $scope.col.footerCellClass;
846 }
847 contents.addClass(classAdded);
848 };
849
850 if ($scope.col.footerCellClass) {
851 updateClass();
852 }
853
854 $scope.col.updateAggregationValue();
855
856 // Watch for column changes so we can alter the col cell class properly
857 /* shouldn't be needed any more, given track by col.name
858 $scope.$watch('col', function (n, o) {
859 if (n !== o) {
860 // See if the column's internal class has changed
861 var newColClass = $scope.col.getColClass(false);
862 if (newColClass !== initColClass) {
863 $elm.removeClass(initColClass);
864 $elm.addClass(newColClass);
865 initColClass = newColClass;
866 }
867 }
868 });
869 */
870
871
872 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
873 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]);
874 // listen for visible rows change and update aggregation values
875 $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue );
876 $scope.grid.api.core.on.rowsRendered( $scope, updateClass );
877 $scope.$on( '$destroy', dataChangeDereg );
878 }
879 };
880 }
881 };
882
883 return uiGridFooterCell;
884 }]);
885
886 })();
887
888 (function () {
889 'use strict';
890
891 angular.module('ui.grid').directive('uiGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
892
893 return {
894 restrict: 'EA',
895 replace: true,
896 // priority: 1000,
897 require: ['^uiGrid', '^uiGridRenderContainer'],
898 scope: true,
899 compile: function ($elm, $attrs) {
900 return {
901 pre: function ($scope, $elm, $attrs, controllers) {
902 var uiGridCtrl = controllers[0];
903 var containerCtrl = controllers[1];
904
905 $scope.grid = uiGridCtrl.grid;
906 $scope.colContainer = containerCtrl.colContainer;
907
908 containerCtrl.footer = $elm;
909
910 var footerTemplate = $scope.grid.options.footerTemplate;
911 gridUtil.getTemplate(footerTemplate)
912 .then(function (contents) {
913 var template = angular.element(contents);
914
915 var newElm = $compile(template)($scope);
916 $elm.append(newElm);
917
918 if (containerCtrl) {
919 // Inject a reference to the footer viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
920 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
921
922 if (footerViewport) {
923 containerCtrl.footerViewport = footerViewport;
924 }
925 }
926 });
927 },
928
929 post: function ($scope, $elm, $attrs, controllers) {
930 var uiGridCtrl = controllers[0];
931 var containerCtrl = controllers[1];
932
933 // gridUtil.logDebug('ui-grid-footer link');
934
935 var grid = uiGridCtrl.grid;
936
937 // Don't animate footer cells
938 gridUtil.disableAnimations($elm);
939
940 containerCtrl.footer = $elm;
941
942 var footerViewport = $elm[0].getElementsByClassName('ui-grid-footer-viewport')[0];
943 if (footerViewport) {
944 containerCtrl.footerViewport = footerViewport;
945 }
946 }
947 };
948 }
949 };
950 }]);
951
952 })();
953 (function () {
954 'use strict';
955
956 angular.module('ui.grid').directive('uiGridGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) {
957
958 return {
959 restrict: 'EA',
960 replace: true,
961 // priority: 1000,
962 require: '^uiGrid',
963 scope: true,
964 compile: function ($elm, $attrs) {
965 return {
966 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
967
968 $scope.grid = uiGridCtrl.grid;
969
970
971
972 var footerTemplate = $scope.grid.options.gridFooterTemplate;
973 gridUtil.getTemplate(footerTemplate)
974 .then(function (contents) {
975 var template = angular.element(contents);
976
977 var newElm = $compile(template)($scope);
978 $elm.append(newElm);
979 });
980 },
981
982 post: function ($scope, $elm, $attrs, controllers) {
983
984 }
985 };
986 }
987 };
988 }]);
989
990 })();
991 (function(){
992 'use strict';
993
994 angular.module('ui.grid').directive('uiGridGroupPanel', ["$compile", "uiGridConstants", "gridUtil", function($compile, uiGridConstants, gridUtil) {
995 var defaultTemplate = 'ui-grid/ui-grid-group-panel';
996
997 return {
998 restrict: 'EA',
999 replace: true,
1000 require: '?^uiGrid',
1001 scope: false,
1002 compile: function($elm, $attrs) {
1003 return {
1004 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
1005 var groupPanelTemplate = $scope.grid.options.groupPanelTemplate || defaultTemplate;
1006
1007 gridUtil.getTemplate(groupPanelTemplate)
1008 .then(function (contents) {
1009 var template = angular.element(contents);
1010
1011 var newElm = $compile(template)($scope);
1012 $elm.append(newElm);
1013 });
1014 },
1015
1016 post: function ($scope, $elm, $attrs, uiGridCtrl) {
1017 $elm.bind('$destroy', function() {
1018 // scrollUnbinder();
1019 });
1020 }
1021 };
1022 }
1023 };
1024 }]);
1025
1026 })();
1027 (function(){
1028 'use strict';
1029
1030 angular.module('ui.grid').directive('uiGridHeaderCell', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'ScrollEvent', 'i18nService',
1031 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent, i18nService) {
1032 // Do stuff after mouse has been down this many ms on the header cell
1033 var mousedownTimeout = 500;
1034 var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa
1035
1036 var uiGridHeaderCell = {
1037 priority: 0,
1038 scope: {
1039 col: '=',
1040 row: '=',
1041 renderIndex: '='
1042 },
1043 require: ['^uiGrid', '^uiGridRenderContainer'],
1044 replace: true,
1045 compile: function() {
1046 return {
1047 pre: function ($scope, $elm, $attrs) {
1048 var cellHeader = $compile($scope.col.headerCellTemplate)($scope);
1049 $elm.append(cellHeader);
1050 },
1051
1052 post: function ($scope, $elm, $attrs, controllers) {
1053 var uiGridCtrl = controllers[0];
1054 var renderContainerCtrl = controllers[1];
1055
1056 $scope.i18n = {
1057 headerCell: i18nService.getSafeText('headerCell'),
1058 sort: i18nService.getSafeText('sort')
1059 };
1060 $scope.getSortDirectionAriaLabel = function(){
1061 var col = $scope.col;
1062 //Trying to recreate this sort of thing but it was getting messy having it in the template.
1063 //Sort direction {{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending':'none')}}. {{col.sort.priority ? {{columnPriorityText}} {{col.sort.priority}} : ''}
1064 var sortDirectionText = col.sort.direction === uiGridConstants.ASC ? $scope.i18n.sort.ascending : ( col.sort.direction === uiGridConstants.DESC ? $scope.i18n.sort.descending : $scope.i18n.sort.none);
1065 var label = sortDirectionText;
1066 //Append the priority if it exists
1067 if (col.sort.priority) {
1068 label = label + '. ' + $scope.i18n.headerCell.priority + ' ' + col.sort.priority;
1069 }
1070 return label;
1071 };
1072
1073 $scope.grid = uiGridCtrl.grid;
1074
1075 $scope.renderContainer = uiGridCtrl.grid.renderContainers[renderContainerCtrl.containerId];
1076
1077 var initColClass = $scope.col.getColClass(false);
1078 $elm.addClass(initColClass);
1079
1080 // Hide the menu by default
1081 $scope.menuShown = false;
1082
1083 // Put asc and desc sort directions in scope
1084 $scope.asc = uiGridConstants.ASC;
1085 $scope.desc = uiGridConstants.DESC;
1086
1087 // Store a reference to menu element
1088 var $colMenu = angular.element( $elm[0].querySelectorAll('.ui-grid-header-cell-menu') );
1089
1090 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
1091
1092
1093 // apply any headerCellClass
1094 var classAdded;
1095 var previousMouseX;
1096
1097 // filter watchers
1098 var filterDeregisters = [];
1099
1100
1101 /*
1102 * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart).
1103 * Once we have a down event, we need to work out whether we have a click, a drag, or a
1104 * hold. A click would sort the grid (if sortable). A drag would be used by moveable, so
1105 * we ignore it. A hold would open the menu.
1106 *
1107 * So, on down event, we put in place handlers for move and up events, and a timer. If the
1108 * timer expires before we see a move or up, then we have a long press and hence a column menu open.
1109 * If the up happens before the timer, then we have a click, and we sort if the column is sortable.
1110 * If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature
1111 * will handle it.
1112 *
1113 * To deal with touch enabled devices that also have mice, we only create our handlers when
1114 * we get the down event, and we create the corresponding handlers - if we're touchstart then
1115 * we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup.
1116 *
1117 * We also suppress the click action whilst this is happening - otherwise after the mouseup there
1118 * will be a click event and that can cause the column menu to close
1119 *
1120 */
1121
1122 $scope.downFn = function( event ){
1123 event.stopPropagation();
1124
1125 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
1126 event = event.originalEvent;
1127 }
1128
1129 // Don't show the menu if it's not the left button
1130 if (event.button && event.button !== 0) {
1131 return;
1132 }
1133 previousMouseX = event.pageX;
1134
1135 $scope.mousedownStartTime = (new Date()).getTime();
1136 $scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout);
1137
1138 $scope.mousedownTimeout.then(function () {
1139 if ( $scope.colMenu ) {
1140 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event);
1141 }
1142 });
1143
1144 uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name});
1145
1146 $scope.offAllEvents();
1147 if ( event.type === 'touchstart'){
1148 $document.on('touchend', $scope.upFn);
1149 $document.on('touchmove', $scope.moveFn);
1150 } else if ( event.type === 'mousedown' ){
1151 $document.on('mouseup', $scope.upFn);
1152 $document.on('mousemove', $scope.moveFn);
1153 }
1154 };
1155
1156 $scope.upFn = function( event ){
1157 event.stopPropagation();
1158 $timeout.cancel($scope.mousedownTimeout);
1159 $scope.offAllEvents();
1160 $scope.onDownEvents(event.type);
1161
1162 var mousedownEndTime = (new Date()).getTime();
1163 var mousedownTime = mousedownEndTime - $scope.mousedownStartTime;
1164
1165 if (mousedownTime > mousedownTimeout) {
1166 // long click, handled above with mousedown
1167 }
1168 else {
1169 // short click
1170 if ( $scope.sortable ){
1171 $scope.handleClick(event);
1172 }
1173 }
1174 };
1175
1176 $scope.moveFn = function( event ){
1177 // Chrome is known to fire some bogus move events.
1178 var changeValue = event.pageX - previousMouseX;
1179 if ( changeValue === 0 ){ return; }
1180
1181 // we're a move, so do nothing and leave for column move (if enabled) to take over
1182 $timeout.cancel($scope.mousedownTimeout);
1183 $scope.offAllEvents();
1184 $scope.onDownEvents(event.type);
1185 };
1186
1187 $scope.clickFn = function ( event ){
1188 event.stopPropagation();
1189 $contentsElm.off('click', $scope.clickFn);
1190 };
1191
1192
1193 $scope.offAllEvents = function(){
1194 $contentsElm.off('touchstart', $scope.downFn);
1195 $contentsElm.off('mousedown', $scope.downFn);
1196
1197 $document.off('touchend', $scope.upFn);
1198 $document.off('mouseup', $scope.upFn);
1199
1200 $document.off('touchmove', $scope.moveFn);
1201 $document.off('mousemove', $scope.moveFn);
1202
1203 $contentsElm.off('click', $scope.clickFn);
1204 };
1205
1206 $scope.onDownEvents = function( type ){
1207 // If there is a previous event, then wait a while before
1208 // activating the other mode - i.e. if the last event was a touch event then
1209 // don't enable mouse events for a wee while (500ms or so)
1210 // Avoids problems with devices that emulate mouse events when you have touch events
1211
1212 switch (type){
1213 case 'touchmove':
1214 case 'touchend':
1215 $contentsElm.on('click', $scope.clickFn);
1216 $contentsElm.on('touchstart', $scope.downFn);
1217 $timeout(function(){
1218 $contentsElm.on('mousedown', $scope.downFn);
1219 }, changeModeTimeout);
1220 break;
1221 case 'mousemove':
1222 case 'mouseup':
1223 $contentsElm.on('click', $scope.clickFn);
1224 $contentsElm.on('mousedown', $scope.downFn);
1225 $timeout(function(){
1226 $contentsElm.on('touchstart', $scope.downFn);
1227 }, changeModeTimeout);
1228 break;
1229 default:
1230 $contentsElm.on('click', $scope.clickFn);
1231 $contentsElm.on('touchstart', $scope.downFn);
1232 $contentsElm.on('mousedown', $scope.downFn);
1233 }
1234 };
1235
1236
1237 var updateHeaderOptions = function( grid ){
1238 var contents = $elm;
1239 if ( classAdded ){
1240 contents.removeClass( classAdded );
1241 classAdded = null;
1242 }
1243
1244 if (angular.isFunction($scope.col.headerCellClass)) {
1245 classAdded = $scope.col.headerCellClass($scope.grid, $scope.row, $scope.col, $scope.rowRenderIndex, $scope.colRenderIndex);
1246 }
1247 else {
1248 classAdded = $scope.col.headerCellClass;
1249 }
1250 contents.addClass(classAdded);
1251
1252 var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body'];
1253 $scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] );
1254
1255 // Figure out whether this column is sortable or not
1256 if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) {
1257 $scope.sortable = true;
1258 }
1259 else {
1260 $scope.sortable = false;
1261 }
1262
1263 // Figure out whether this column is filterable or not
1264 var oldFilterable = $scope.filterable;
1265 if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) {
1266 $scope.filterable = true;
1267 }
1268 else {
1269 $scope.filterable = false;
1270 }
1271
1272 if ( oldFilterable !== $scope.filterable){
1273 if ( typeof($scope.col.updateFilters) !== 'undefined' ){
1274 $scope.col.updateFilters($scope.filterable);
1275 }
1276
1277 // if column is filterable add a filter watcher
1278 if ($scope.filterable) {
1279 $scope.col.filters.forEach( function(filter, i) {
1280 filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) {
1281 if (n !== o) {
1282 uiGridCtrl.grid.api.core.raise.filterChanged();
1283 uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1284 uiGridCtrl.grid.queueGridRefresh();
1285 }
1286 }));
1287 });
1288 $scope.$on('$destroy', function() {
1289 filterDeregisters.forEach( function(filterDeregister) {
1290 filterDeregister();
1291 });
1292 });
1293 } else {
1294 filterDeregisters.forEach( function(filterDeregister) {
1295 filterDeregister();
1296 });
1297 }
1298
1299 }
1300
1301 // figure out whether we support column menus
1302 if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false &&
1303 $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){
1304 $scope.colMenu = true;
1305 } else {
1306 $scope.colMenu = false;
1307 }
1308
1309 /**
1310 * @ngdoc property
1311 * @name enableColumnMenu
1312 * @propertyOf ui.grid.class:GridOptions.columnDef
1313 * @description if column menus are enabled, controls the column menus for this specific
1314 * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus
1315 * using this option. If gridOptions.enableColumnMenus === false then you get no column
1316 * menus irrespective of the value of this option ). Defaults to true.
1317 *
1318 */
1319 /**
1320 * @ngdoc property
1321 * @name enableColumnMenus
1322 * @propertyOf ui.grid.class:GridOptions.columnDef
1323 * @description Override for column menus everywhere - if set to false then you get no
1324 * column menus. Defaults to true.
1325 *
1326 */
1327
1328 $scope.offAllEvents();
1329
1330 if ($scope.sortable || $scope.colMenu) {
1331 $scope.onDownEvents();
1332
1333 $scope.$on('$destroy', function () {
1334 $scope.offAllEvents();
1335 });
1336 }
1337 };
1338
1339 /*
1340 $scope.$watch('col', function (n, o) {
1341 if (n !== o) {
1342 // See if the column's internal class has changed
1343 var newColClass = $scope.col.getColClass(false);
1344 if (newColClass !== initColClass) {
1345 $elm.removeClass(initColClass);
1346 $elm.addClass(newColClass);
1347 initColClass = newColClass;
1348 }
1349 }
1350 });
1351 */
1352 updateHeaderOptions();
1353
1354 // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs
1355 var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]);
1356
1357 $scope.$on( '$destroy', dataChangeDereg );
1358
1359 $scope.handleClick = function(event) {
1360 // If the shift key is being held down, add this column to the sort
1361 var add = false;
1362 if (event.shiftKey) {
1363 add = true;
1364 }
1365
1366 // Sort this column then rebuild the grid's rows
1367 uiGridCtrl.grid.sortColumn($scope.col, add)
1368 .then(function () {
1369 if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); }
1370 uiGridCtrl.grid.refresh();
1371 });
1372 };
1373
1374
1375 $scope.toggleMenu = function(event) {
1376 event.stopPropagation();
1377
1378 // If the menu is already showing...
1379 if (uiGridCtrl.columnMenuScope.menuShown) {
1380 // ... and we're the column the menu is on...
1381 if (uiGridCtrl.columnMenuScope.col === $scope.col) {
1382 // ... hide it
1383 uiGridCtrl.columnMenuScope.hideMenu();
1384 }
1385 // ... and we're NOT the column the menu is on
1386 else {
1387 // ... move the menu to our column
1388 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1389 }
1390 }
1391 // If the menu is NOT showing
1392 else {
1393 // ... show it on our column
1394 uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm);
1395 }
1396 };
1397 }
1398 };
1399 }
1400 };
1401
1402 return uiGridHeaderCell;
1403 }]);
1404
1405 })();
1406
1407 (function(){
1408 'use strict';
1409
1410 angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', 'ScrollEvent',
1411 function($templateCache, $compile, uiGridConstants, gridUtil, $timeout, ScrollEvent) {
1412 var defaultTemplate = 'ui-grid/ui-grid-header';
1413 var emptyTemplate = 'ui-grid/ui-grid-no-header';
1414
1415 return {
1416 restrict: 'EA',
1417 // templateUrl: 'ui-grid/ui-grid-header',
1418 replace: true,
1419 // priority: 1000,
1420 require: ['^uiGrid', '^uiGridRenderContainer'],
1421 scope: true,
1422 compile: function($elm, $attrs) {
1423 return {
1424 pre: function ($scope, $elm, $attrs, controllers) {
1425 var uiGridCtrl = controllers[0];
1426 var containerCtrl = controllers[1];
1427
1428 $scope.grid = uiGridCtrl.grid;
1429 $scope.colContainer = containerCtrl.colContainer;
1430
1431 updateHeaderReferences();
1432
1433 var headerTemplate;
1434 if (!$scope.grid.options.showHeader) {
1435 headerTemplate = emptyTemplate;
1436 }
1437 else {
1438 headerTemplate = ($scope.grid.options.headerTemplate) ? $scope.grid.options.headerTemplate : defaultTemplate;
1439 }
1440
1441 gridUtil.getTemplate(headerTemplate)
1442 .then(function (contents) {
1443 var template = angular.element(contents);
1444
1445 var newElm = $compile(template)($scope);
1446 $elm.replaceWith(newElm);
1447
1448 // And update $elm to be the new element
1449 $elm = newElm;
1450
1451 updateHeaderReferences();
1452
1453 if (containerCtrl) {
1454 // Inject a reference to the header viewport (if it exists) into the grid controller for use in the horizontal scroll handler below
1455 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1456
1457
1458 if (headerViewport) {
1459 containerCtrl.headerViewport = headerViewport;
1460 angular.element(headerViewport).on('scroll', scrollHandler);
1461 $scope.$on('$destroy', function () {
1462 angular.element(headerViewport).off('scroll', scrollHandler);
1463 });
1464 }
1465 }
1466
1467 $scope.grid.queueRefresh();
1468 });
1469
1470 function updateHeaderReferences() {
1471 containerCtrl.header = containerCtrl.colContainer.header = $elm;
1472
1473 var headerCanvases = $elm[0].getElementsByClassName('ui-grid-header-canvas');
1474
1475 if (headerCanvases.length > 0) {
1476 containerCtrl.headerCanvas = containerCtrl.colContainer.headerCanvas = headerCanvases[0];
1477 }
1478 else {
1479 containerCtrl.headerCanvas = null;
1480 }
1481 }
1482
1483 function scrollHandler(evt) {
1484 if (uiGridCtrl.grid.isScrollingHorizontally) {
1485 return;
1486 }
1487 var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport, uiGridCtrl.grid);
1488 var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft);
1489
1490 var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll);
1491 scrollEvent.newScrollLeft = newScrollLeft;
1492 if ( horizScrollPercentage > -1 ){
1493 scrollEvent.x = { percentage: horizScrollPercentage };
1494 }
1495
1496 uiGridCtrl.grid.scrollContainers(null, scrollEvent);
1497 }
1498 },
1499
1500 post: function ($scope, $elm, $attrs, controllers) {
1501 var uiGridCtrl = controllers[0];
1502 var containerCtrl = controllers[1];
1503
1504 // gridUtil.logDebug('ui-grid-header link');
1505
1506 var grid = uiGridCtrl.grid;
1507
1508 // Don't animate header cells
1509 gridUtil.disableAnimations($elm);
1510
1511 function updateColumnWidths() {
1512 // this styleBuilder always runs after the renderContainer, so we can rely on the column widths
1513 // already being populated correctly
1514
1515 var columnCache = containerCtrl.colContainer.visibleColumnCache;
1516
1517 // Build the CSS
1518 // uiGridCtrl.grid.columns.forEach(function (column) {
1519 var ret = '';
1520 var canvasWidth = 0;
1521 columnCache.forEach(function (column) {
1522 ret = ret + column.getColClassDefinition();
1523 canvasWidth += column.drawnWidth;
1524 });
1525
1526 containerCtrl.colContainer.canvasWidth = canvasWidth;
1527
1528 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
1529 return ret;
1530 }
1531
1532 containerCtrl.header = $elm;
1533
1534 var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0];
1535 if (headerViewport) {
1536 containerCtrl.headerViewport = headerViewport;
1537 }
1538
1539 //todo: remove this if by injecting gridCtrl into unit tests
1540 if (uiGridCtrl) {
1541 uiGridCtrl.grid.registerStyleComputation({
1542 priority: 15,
1543 func: updateColumnWidths
1544 });
1545 }
1546 }
1547 };
1548 }
1549 };
1550 }]);
1551
1552 })();
1553
1554 (function(){
1555
1556 angular.module('ui.grid')
1557 .service('uiGridGridMenuService', [ 'gridUtil', 'i18nService', 'uiGridConstants', function( gridUtil, i18nService, uiGridConstants ) {
1558 /**
1559 * @ngdoc service
1560 * @name ui.grid.gridMenuService
1561 *
1562 * @description Methods for working with the grid menu
1563 */
1564
1565 var service = {
1566 /**
1567 * @ngdoc method
1568 * @methodOf ui.grid.gridMenuService
1569 * @name initialize
1570 * @description Sets up the gridMenu. Most importantly, sets our
1571 * scope onto the grid object as grid.gridMenuScope, allowing us
1572 * to operate when passed only the grid. Second most importantly,
1573 * we register the 'addToGridMenu' and 'removeFromGridMenu' methods
1574 * on the core api.
1575 * @param {$scope} $scope the scope of this gridMenu
1576 * @param {Grid} grid the grid to which this gridMenu is associated
1577 */
1578 initialize: function( $scope, grid ){
1579 grid.gridMenuScope = $scope;
1580 $scope.grid = grid;
1581 $scope.registeredMenuItems = [];
1582
1583 // not certain this is needed, but would be bad to create a memory leak
1584 $scope.$on('$destroy', function() {
1585 if ( $scope.grid && $scope.grid.gridMenuScope ){
1586 $scope.grid.gridMenuScope = null;
1587 }
1588 if ( $scope.grid ){
1589 $scope.grid = null;
1590 }
1591 if ( $scope.registeredMenuItems ){
1592 $scope.registeredMenuItems = null;
1593 }
1594 });
1595
1596 $scope.registeredMenuItems = [];
1597
1598 /**
1599 * @ngdoc function
1600 * @name addToGridMenu
1601 * @methodOf ui.grid.core.api:PublicApi
1602 * @description add items to the grid menu. Used by features
1603 * to add their menu items if they are enabled, can also be used by
1604 * end users to add menu items. This method has the advantage of allowing
1605 * remove again, which can simplify management of which items are included
1606 * in the menu when. (Noting that in most cases the shown and active functions
1607 * provide a better way to handle visibility of menu items)
1608 * @param {Grid} grid the grid on which we are acting
1609 * @param {array} items menu items in the format as described in the tutorial, with
1610 * the added note that if you want to use remove you must also specify an `id` field,
1611 * which is provided when you want to remove an item. The id should be unique.
1612 *
1613 */
1614 grid.api.registerMethod( 'core', 'addToGridMenu', service.addToGridMenu );
1615
1616 /**
1617 * @ngdoc function
1618 * @name removeFromGridMenu
1619 * @methodOf ui.grid.core.api:PublicApi
1620 * @description Remove an item from the grid menu based on a provided id. Assumes
1621 * that the id is unique, removes only the last instance of that id. Does nothing if
1622 * the specified id is not found
1623 * @param {Grid} grid the grid on which we are acting
1624 * @param {string} id the id we'd like to remove from the menu
1625 *
1626 */
1627 grid.api.registerMethod( 'core', 'removeFromGridMenu', service.removeFromGridMenu );
1628 },
1629
1630
1631 /**
1632 * @ngdoc function
1633 * @name addToGridMenu
1634 * @propertyOf ui.grid.gridMenuService
1635 * @description add items to the grid menu. Used by features
1636 * to add their menu items if they are enabled, can also be used by
1637 * end users to add menu items. This method has the advantage of allowing
1638 * remove again, which can simplify management of which items are included
1639 * in the menu when. (Noting that in most cases the shown and active functions
1640 * provide a better way to handle visibility of menu items)
1641 * @param {Grid} grid the grid on which we are acting
1642 * @param {array} items menu items in the format as described in the tutorial, with
1643 * the added note that if you want to use remove you must also specify an `id` field,
1644 * which is provided when you want to remove an item. The id should be unique.
1645 *
1646 */
1647 addToGridMenu: function( grid, menuItems ) {
1648 if ( !angular.isArray( menuItems ) ) {
1649 gridUtil.logError( 'addToGridMenu: menuItems must be an array, and is not, not adding any items');
1650 } else {
1651 if ( grid.gridMenuScope ){
1652 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems ? grid.gridMenuScope.registeredMenuItems : [];
1653 grid.gridMenuScope.registeredMenuItems = grid.gridMenuScope.registeredMenuItems.concat( menuItems );
1654 } else {
1655 gridUtil.logError( 'Asked to addToGridMenu, but gridMenuScope not present. Timing issue? Please log issue with ui-grid');
1656 }
1657 }
1658 },
1659
1660
1661 /**
1662 * @ngdoc function
1663 * @name removeFromGridMenu
1664 * @methodOf ui.grid.gridMenuService
1665 * @description Remove an item from the grid menu based on a provided id. Assumes
1666 * that the id is unique, removes only the last instance of that id. Does nothing if
1667 * the specified id is not found. If there is no gridMenuScope or registeredMenuItems
1668 * then do nothing silently - the desired result is those menu items not be present and they
1669 * aren't.
1670 * @param {Grid} grid the grid on which we are acting
1671 * @param {string} id the id we'd like to remove from the menu
1672 *
1673 */
1674 removeFromGridMenu: function( grid, id ){
1675 var foundIndex = -1;
1676
1677 if ( grid && grid.gridMenuScope ){
1678 grid.gridMenuScope.registeredMenuItems.forEach( function( value, index ) {
1679 if ( value.id === id ){
1680 if (foundIndex > -1) {
1681 gridUtil.logError( 'removeFromGridMenu: found multiple items with the same id, removing only the last' );
1682 } else {
1683
1684 foundIndex = index;
1685 }
1686 }
1687 });
1688 }
1689
1690 if ( foundIndex > -1 ){
1691 grid.gridMenuScope.registeredMenuItems.splice( foundIndex, 1 );
1692 }
1693 },
1694
1695
1696 /**
1697 * @ngdoc array
1698 * @name gridMenuCustomItems
1699 * @propertyOf ui.grid.class:GridOptions
1700 * @description (optional) An array of menu items that should be added to
1701 * the gridMenu. Follow the format documented in the tutorial for column
1702 * menu customisation. The context provided to the action function will
1703 * include context.grid. An alternative if working with dynamic menus is to use the
1704 * provided api - core.addToGridMenu and core.removeFromGridMenu, which handles
1705 * some of the management of items for you.
1706 *
1707 */
1708 /**
1709 * @ngdoc boolean
1710 * @name gridMenuShowHideColumns
1711 * @propertyOf ui.grid.class:GridOptions
1712 * @description true by default, whether the grid menu should allow hide/show
1713 * of columns
1714 *
1715 */
1716 /**
1717 * @ngdoc method
1718 * @methodOf ui.grid.gridMenuService
1719 * @name getMenuItems
1720 * @description Decides the menu items to show in the menu. This is a
1721 * combination of:
1722 *
1723 * - the default menu items that are always included,
1724 * - any menu items that have been provided through the addMenuItem api. These
1725 * are typically added by features within the grid
1726 * - any menu items included in grid.options.gridMenuCustomItems. These can be
1727 * changed dynamically, as they're always recalculated whenever we show the
1728 * menu
1729 * @param {$scope} $scope the scope of this gridMenu, from which we can find all
1730 * the information that we need
1731 * @returns {array} an array of menu items that can be shown
1732 */
1733 getMenuItems: function( $scope ) {
1734 var menuItems = [
1735 // this is where we add any menu items we want to always include
1736 ];
1737
1738 if ( $scope.grid.options.gridMenuCustomItems ){
1739 if ( !angular.isArray( $scope.grid.options.gridMenuCustomItems ) ){
1740 gridUtil.logError( 'gridOptions.gridMenuCustomItems must be an array, and is not');
1741 } else {
1742 menuItems = menuItems.concat( $scope.grid.options.gridMenuCustomItems );
1743 }
1744 }
1745
1746 var clearFilters = [{
1747 title: i18nService.getSafeText('gridMenu.clearAllFilters'),
1748 action: function ($event) {
1749 $scope.grid.clearAllFilters(undefined, true, undefined);
1750 },
1751 shown: function() {
1752 return $scope.grid.options.enableFiltering;
1753 },
1754 order: 100
1755 }];
1756 menuItems = menuItems.concat( clearFilters );
1757
1758 menuItems = menuItems.concat( $scope.registeredMenuItems );
1759
1760 if ( $scope.grid.options.gridMenuShowHideColumns !== false ){
1761 menuItems = menuItems.concat( service.showHideColumns( $scope ) );
1762 }
1763
1764 menuItems.sort(function(a, b){
1765 return a.order - b.order;
1766 });
1767
1768 return menuItems;
1769 },
1770
1771
1772 /**
1773 * @ngdoc array
1774 * @name gridMenuTitleFilter
1775 * @propertyOf ui.grid.class:GridOptions
1776 * @description (optional) A function that takes a title string
1777 * (usually the col.displayName), and converts it into a display value. The function
1778 * must return either a string or a promise.
1779 *
1780 * Used for internationalization of the grid menu column names - for angular-translate
1781 * you can pass $translate as the function, for i18nService you can pass getSafeText as the
1782 * function
1783 * @example
1784 * <pre>
1785 * gridOptions = {
1786 * gridMenuTitleFilter: $translate
1787 * }
1788 * </pre>
1789 */
1790 /**
1791 * @ngdoc method
1792 * @methodOf ui.grid.gridMenuService
1793 * @name showHideColumns
1794 * @description Adds two menu items for each of the columns in columnDefs. One
1795 * menu item for hide, one menu item for show. Each is visible when appropriate
1796 * (show when column is not visible, hide when column is visible). Each toggles
1797 * the visible property on the columnDef using toggleColumnVisibility
1798 * @param {$scope} $scope of a gridMenu, which contains a reference to the grid
1799 */
1800 showHideColumns: function( $scope ){
1801 var showHideColumns = [];
1802 if ( !$scope.grid.options.columnDefs || $scope.grid.options.columnDefs.length === 0 || $scope.grid.columns.length === 0 ) {
1803 return showHideColumns;
1804 }
1805
1806 // add header for columns
1807 showHideColumns.push({
1808 title: i18nService.getSafeText('gridMenu.columns'),
1809 order: 300
1810 });
1811
1812 $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; };
1813
1814 $scope.grid.options.columnDefs.forEach( function( colDef, index ){
1815 if ( colDef.enableHiding !== false ){
1816 // add hide menu item - shows an OK icon as we only show when column is already visible
1817 var menuItem = {
1818 icon: 'ui-grid-icon-ok',
1819 action: function($event) {
1820 $event.stopPropagation();
1821 service.toggleColumnVisibility( this.context.gridCol );
1822 },
1823 shown: function() {
1824 return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined;
1825 },
1826 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1827 leaveOpen: true,
1828 order: 301 + index * 2
1829 };
1830 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1831 showHideColumns.push( menuItem );
1832
1833 // add show menu item - shows no icon as we only show when column is invisible
1834 menuItem = {
1835 icon: 'ui-grid-icon-cancel',
1836 action: function($event) {
1837 $event.stopPropagation();
1838 service.toggleColumnVisibility( this.context.gridCol );
1839 },
1840 shown: function() {
1841 return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined);
1842 },
1843 context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) },
1844 leaveOpen: true,
1845 order: 301 + index * 2 + 1
1846 };
1847 service.setMenuItemTitle( menuItem, colDef, $scope.grid );
1848 showHideColumns.push( menuItem );
1849 }
1850 });
1851 return showHideColumns;
1852 },
1853
1854
1855 /**
1856 * @ngdoc method
1857 * @methodOf ui.grid.gridMenuService
1858 * @name setMenuItemTitle
1859 * @description Handles the response from gridMenuTitleFilter, adding it directly to the menu
1860 * item if it returns a string, otherwise waiting for the promise to resolve or reject then
1861 * putting the result into the title
1862 * @param {object} menuItem the menuItem we want to put the title on
1863 * @param {object} colDef the colDef from which we can get displayName, name or field
1864 * @param {Grid} grid the grid, from which we can get the options.gridMenuTitleFilter
1865 *
1866 */
1867 setMenuItemTitle: function( menuItem, colDef, grid ){
1868 var title = grid.options.gridMenuTitleFilter( colDef.displayName || gridUtil.readableColumnName(colDef.name) || colDef.field );
1869
1870 if ( typeof(title) === 'string' ){
1871 menuItem.title = title;
1872 } else if ( title.then ){
1873 // must be a promise
1874 menuItem.title = "";
1875 title.then( function( successValue ) {
1876 menuItem.title = successValue;
1877 }, function( errorValue ) {
1878 menuItem.title = errorValue;
1879 });
1880 } else {
1881 gridUtil.logError('Expected gridMenuTitleFilter to return a string or a promise, it has returned neither, bad config');
1882 menuItem.title = 'badconfig';
1883 }
1884 },
1885
1886 /**
1887 * @ngdoc method
1888 * @methodOf ui.grid.gridMenuService
1889 * @name toggleColumnVisibility
1890 * @description Toggles the visibility of an individual column. Expects to be
1891 * provided a context that has on it a gridColumn, which is the column that
1892 * we'll operate upon. We change the visibility, and refresh the grid as appropriate
1893 * @param {GridCol} gridCol the column that we want to toggle
1894 *
1895 */
1896 toggleColumnVisibility: function( gridCol ) {
1897 gridCol.colDef.visible = !( gridCol.colDef.visible === true || gridCol.colDef.visible === undefined );
1898
1899 gridCol.grid.refresh();
1900 gridCol.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
1901 gridCol.grid.api.core.raise.columnVisibilityChanged( gridCol );
1902 }
1903 };
1904
1905 return service;
1906 }])
1907
1908
1909
1910 .directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', 'i18nService',
1911 function (gridUtil, uiGridConstants, uiGridGridMenuService, i18nService) {
1912
1913 return {
1914 priority: 0,
1915 scope: true,
1916 require: ['^uiGrid'],
1917 templateUrl: 'ui-grid/ui-grid-menu-button',
1918 replace: true,
1919
1920 link: function ($scope, $elm, $attrs, controllers) {
1921 var uiGridCtrl = controllers[0];
1922
1923 // For the aria label
1924 $scope.i18n = {
1925 aria: i18nService.getSafeText('gridMenu.aria')
1926 };
1927
1928 uiGridGridMenuService.initialize($scope, uiGridCtrl.grid);
1929
1930 $scope.shown = false;
1931
1932 $scope.toggleMenu = function () {
1933 if ( $scope.shown ){
1934 $scope.$broadcast('hide-menu');
1935 $scope.shown = false;
1936 } else {
1937 $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope );
1938 $scope.$broadcast('show-menu');
1939 $scope.shown = true;
1940 }
1941 };
1942
1943 $scope.$on('menu-hidden', function() {
1944 $scope.shown = false;
1945 gridUtil.focus.bySelector($elm, '.ui-grid-icon-container');
1946 });
1947 }
1948 };
1949
1950 }]);
1951
1952 })();
1953
1954 (function(){
1955
1956 /**
1957 * @ngdoc directive
1958 * @name ui.grid.directive:uiGridMenu
1959 * @element style
1960 * @restrict A
1961 *
1962 * @description
1963 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
1964 *
1965 * @example
1966 <doc:example module="app">
1967 <doc:source>
1968 <script>
1969 var app = angular.module('app', ['ui.grid']);
1970
1971 app.controller('MainCtrl', ['$scope', function ($scope) {
1972
1973 }]);
1974 </script>
1975
1976 <div ng-controller="MainCtrl">
1977 <div ui-grid-menu shown="true" ></div>
1978 </div>
1979 </doc:source>
1980 <doc:scenario>
1981 </doc:scenario>
1982 </doc:example>
1983 */
1984 angular.module('ui.grid')
1985
1986 .directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', 'i18nService',
1987 function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, i18nService) {
1988 var uiGridMenu = {
1989 priority: 0,
1990 scope: {
1991 // shown: '&',
1992 menuItems: '=',
1993 autoHide: '=?'
1994 },
1995 require: '?^uiGrid',
1996 templateUrl: 'ui-grid/uiGridMenu',
1997 replace: false,
1998 link: function ($scope, $elm, $attrs, uiGridCtrl) {
1999 var self = this;
2000 var menuMid;
2001 var $animate;
2002
2003 $scope.i18n = {
2004 close: i18nService.getSafeText('columnMenu.close')
2005 };
2006
2007 // *** Show/Hide functions ******
2008 self.showMenu = $scope.showMenu = function(event, args) {
2009 if ( !$scope.shown ){
2010
2011 /*
2012 * In order to animate cleanly we remove the ng-if, wait a digest cycle, then
2013 * animate the removal of the ng-hide. We can't successfully (so far as I can tell)
2014 * animate removal of the ng-if, as the menu items aren't there yet. And we don't want
2015 * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated
2016 * on scroll events.
2017 *
2018 * Note when testing animation that animations don't run on the tutorials. When debugging it looks
2019 * like they do, but angular has a default $animate provider that is just a stub, and that's what's
2020 * being called. ALso don't be fooled by the fact that your browser has actually loaded the
2021 * angular-translate.js, it's not using it. You need to test animations in an external application.
2022 */
2023 $scope.shown = true;
2024
2025 $timeout( function() {
2026 $scope.shownMid = true;
2027 $scope.$emit('menu-shown');
2028 });
2029 } else if ( !$scope.shownMid ) {
2030 // we're probably doing a hide then show, so we don't need to wait for ng-if
2031 $scope.shownMid = true;
2032 $scope.$emit('menu-shown');
2033 }
2034
2035 var docEventType = 'click';
2036 if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') {
2037 docEventType = args.originalEvent.type;
2038 }
2039
2040 // Turn off an existing document click handler
2041 angular.element(document).off('click touchstart', applyHideMenu);
2042
2043 // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one
2044 $timeout(function() {
2045 angular.element(document).on(docEventType, applyHideMenu);
2046 });
2047 //automatically set the focus to the first button element in the now open menu.
2048 gridUtil.focus.bySelector($elm, 'button[type=button]', true);
2049 };
2050
2051
2052 self.hideMenu = $scope.hideMenu = function(event, args) {
2053 if ( $scope.shown ){
2054 /*
2055 * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to
2056 * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the
2057 * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason.
2058 *
2059 * The user may have clicked on the menu again whilst
2060 * we're waiting, so we check that the mid isn't shown before applying the ng-if.
2061 */
2062 $scope.shownMid = false;
2063 $timeout( function() {
2064 if ( !$scope.shownMid ){
2065 $scope.shown = false;
2066 $scope.$emit('menu-hidden');
2067 }
2068 }, 200);
2069 }
2070
2071 angular.element(document).off('click touchstart', applyHideMenu);
2072 };
2073
2074 $scope.$on('hide-menu', function (event, args) {
2075 $scope.hideMenu(event, args);
2076 });
2077
2078 $scope.$on('show-menu', function (event, args) {
2079 $scope.showMenu(event, args);
2080 });
2081
2082
2083 // *** Auto hide when click elsewhere ******
2084 var applyHideMenu = function(){
2085 if ($scope.shown) {
2086 $scope.$apply(function () {
2087 $scope.hideMenu();
2088 });
2089 }
2090 };
2091
2092 if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) {
2093 $scope.autoHide = true;
2094 }
2095
2096 if ($scope.autoHide) {
2097 angular.element($window).on('resize', applyHideMenu);
2098 }
2099
2100 $scope.$on('$destroy', function () {
2101 angular.element(document).off('click touchstart', applyHideMenu);
2102 });
2103
2104
2105 $scope.$on('$destroy', function() {
2106 angular.element($window).off('resize', applyHideMenu);
2107 });
2108
2109 if (uiGridCtrl) {
2110 $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollBegin($scope, applyHideMenu ));
2111 }
2112
2113 $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu ));
2114 },
2115
2116
2117 controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
2118 var self = this;
2119 }]
2120 };
2121
2122 return uiGridMenu;
2123 }])
2124
2125 .directive('uiGridMenuItem', ['gridUtil', '$compile', 'i18nService', function (gridUtil, $compile, i18nService) {
2126 var uiGridMenuItem = {
2127 priority: 0,
2128 scope: {
2129 name: '=',
2130 active: '=',
2131 action: '=',
2132 icon: '=',
2133 shown: '=',
2134 context: '=',
2135 templateUrl: '=',
2136 leaveOpen: '=',
2137 screenReaderOnly: '='
2138 },
2139 require: ['?^uiGrid', '^uiGridMenu'],
2140 templateUrl: 'ui-grid/uiGridMenuItem',
2141 replace: false,
2142 compile: function($elm, $attrs) {
2143 return {
2144 pre: function ($scope, $elm, $attrs, controllers) {
2145 var uiGridCtrl = controllers[0],
2146 uiGridMenuCtrl = controllers[1];
2147
2148 if ($scope.templateUrl) {
2149 gridUtil.getTemplate($scope.templateUrl)
2150 .then(function (contents) {
2151 var template = angular.element(contents);
2152
2153 var newElm = $compile(template)($scope);
2154 $elm.replaceWith(newElm);
2155 });
2156 }
2157 },
2158 post: function ($scope, $elm, $attrs, controllers) {
2159 var uiGridCtrl = controllers[0],
2160 uiGridMenuCtrl = controllers[1];
2161
2162 // TODO(c0bra): validate that shown and active are functions if they're defined. An exception is already thrown above this though
2163 // if (typeof($scope.shown) !== 'undefined' && $scope.shown && typeof($scope.shown) !== 'function') {
2164 // throw new TypeError("$scope.shown is defined but not a function");
2165 // }
2166 if (typeof($scope.shown) === 'undefined' || $scope.shown === null) {
2167 $scope.shown = function() { return true; };
2168 }
2169
2170 $scope.itemShown = function () {
2171 var context = {};
2172 if ($scope.context) {
2173 context.context = $scope.context;
2174 }
2175
2176 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2177 context.grid = uiGridCtrl.grid;
2178 }
2179
2180 return $scope.shown.call(context);
2181 };
2182
2183 $scope.itemAction = function($event,title) {
2184 gridUtil.logDebug('itemAction');
2185 $event.stopPropagation();
2186
2187 if (typeof($scope.action) === 'function') {
2188 var context = {};
2189
2190 if ($scope.context) {
2191 context.context = $scope.context;
2192 }
2193
2194 // Add the grid to the function call context if the uiGrid controller is present
2195 if (typeof(uiGridCtrl) !== 'undefined' && uiGridCtrl) {
2196 context.grid = uiGridCtrl.grid;
2197 }
2198
2199 $scope.action.call(context, $event, title);
2200
2201 if ( !$scope.leaveOpen ){
2202 $scope.$emit('hide-menu');
2203 } else {
2204 /*
2205 * XXX: Fix after column refactor
2206 * Ideally the focus would remain on the item.
2207 * However, since there are two menu items that have their 'show' property toggled instead. This is a quick fix.
2208 */
2209 gridUtil.focus.bySelector(angular.element(gridUtil.closestElm($elm, ".ui-grid-menu-items")), 'button[type=button]', true);
2210 }
2211 }
2212 };
2213
2214 $scope.i18n = i18nService.get();
2215 }
2216 };
2217 }
2218 };
2219
2220 return uiGridMenuItem;
2221 }]);
2222
2223 })();
2224
2225 (function(){
2226 'use strict';
2227 /**
2228 * @ngdoc overview
2229 * @name ui.grid.directive:uiGridOneBind
2230 * @summary A group of directives that provide a one time bind to a dom element.
2231 * @description A group of directives that provide a one time bind to a dom element.
2232 * As one time bindings are not supported in Angular 1.2.* this directive provdes this capability.
2233 * This is done to reduce the number of watchers on the dom.
2234 * <br/>
2235 * <h2>Short Example ({@link ui.grid.directive:uiGridOneBindSrc ui-grid-one-bind-src})</h2>
2236 * <pre>
2237 <div ng-init="imageName = 'myImageDir.jpg'">
2238 <img ui-grid-one-bind-src="imageName"></img>
2239 </div>
2240 </pre>
2241 * Will become:
2242 * <pre>
2243 <div ng-init="imageName = 'myImageDir.jpg'">
2244 <img ui-grid-one-bind-src="imageName" src="myImageDir.jpg"></img>
2245 </div>
2246 </pre>
2247 </br>
2248 <h2>Short Example ({@link ui.grid.directive:uiGridOneBindText ui-grid-one-bind-text})</h2>
2249 * <pre>
2250 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text"></div>
2251 </pre>
2252 * Will become:
2253 * <pre>
2254 <div ng-init="text='Add this text'" ui-grid-one-bind-text="text">Add this text</div>
2255 </pre>
2256 </br>
2257 * <b>Note:</b> This behavior is slightly different for the {@link ui.grid.directive:uiGridOneBindIdGrid uiGridOneBindIdGrid}
2258 * and {@link ui.grid.directive:uiGridOneBindAriaLabelledbyGrid uiGridOneBindAriaLabelledbyGrid} directives.
2259 *
2260 */
2261 //https://github.com/joshkurz/Black-Belt-AngularJS-Directives/blob/master/directives/Optimization/oneBind.js
2262 var oneBinders = angular.module('ui.grid');
2263 angular.forEach([
2264 /**
2265 * @ngdoc directive
2266 * @name ui.grid.directive:uiGridOneBindSrc
2267 * @memberof ui.grid.directive:uiGridOneBind
2268 * @element img
2269 * @restrict A
2270 * @param {String} uiGridOneBindSrc The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2271 * @description One time binding for the src dom tag.
2272 *
2273 */
2274 {tag: 'Src', method: 'attr'},
2275 /**
2276 * @ngdoc directive
2277 * @name ui.grid.directive:uiGridOneBindText
2278 * @element div
2279 * @restrict A
2280 * @param {String} uiGridOneBindText The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2281 * @description One time binding for the text dom tag.
2282 */
2283 {tag: 'Text', method: 'text'},
2284 /**
2285 * @ngdoc directive
2286 * @name ui.grid.directive:uiGridOneBindHref
2287 * @element div
2288 * @restrict A
2289 * @param {String} uiGridOneBindHref The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2290 * @description One time binding for the href dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2291 */
2292 {tag: 'Href', method: 'attr'},
2293 /**
2294 * @ngdoc directive
2295 * @name ui.grid.directive:uiGridOneBindClass
2296 * @element div
2297 * @restrict A
2298 * @param {String} uiGridOneBindClass The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2299 * @param {Object} uiGridOneBindClass The object that you want to bind. At least one of the values in the object must be something other than null or undefined for the watcher to be removed.
2300 * this is to prevent the watcher from being removed before the scope is initialized.
2301 * @param {Array} uiGridOneBindClass An array of classes to bind to this element.
2302 * @description One time binding for the class dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2303 */
2304 {tag: 'Class', method: 'addClass'},
2305 /**
2306 * @ngdoc directive
2307 * @name ui.grid.directive:uiGridOneBindHtml
2308 * @element div
2309 * @restrict A
2310 * @param {String} uiGridOneBindHtml The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2311 * @description One time binding for the html method on a dom element. For more information see {@link ui.grid.directive:uiGridOneBind}.
2312 */
2313 {tag: 'Html', method: 'html'},
2314 /**
2315 * @ngdoc directive
2316 * @name ui.grid.directive:uiGridOneBindAlt
2317 * @element div
2318 * @restrict A
2319 * @param {String} uiGridOneBindAlt The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2320 * @description One time binding for the alt dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2321 */
2322 {tag: 'Alt', method: 'attr'},
2323 /**
2324 * @ngdoc directive
2325 * @name ui.grid.directive:uiGridOneBindStyle
2326 * @element div
2327 * @restrict A
2328 * @param {String} uiGridOneBindStyle The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2329 * @description One time binding for the style dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2330 */
2331 {tag: 'Style', method: 'css'},
2332 /**
2333 * @ngdoc directive
2334 * @name ui.grid.directive:uiGridOneBindValue
2335 * @element div
2336 * @restrict A
2337 * @param {String} uiGridOneBindValue The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2338 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2339 */
2340 {tag: 'Value', method: 'attr'},
2341 /**
2342 * @ngdoc directive
2343 * @name ui.grid.directive:uiGridOneBindId
2344 * @element div
2345 * @restrict A
2346 * @param {String} uiGridOneBindId The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2347 * @description One time binding for the value dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2348 */
2349 {tag: 'Id', method: 'attr'},
2350 /**
2351 * @ngdoc directive
2352 * @name ui.grid.directive:uiGridOneBindIdGrid
2353 * @element div
2354 * @restrict A
2355 * @param {String} uiGridOneBindIdGrid The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2356 * @description One time binding for the id dom tag.
2357 * <h1>Important Note!</h1>
2358 * If the id tag passed as a parameter does <b>not</b> contain the grid id as a substring
2359 * then the directive will search the scope and the parent controller (if it is a uiGridController) for the grid.id value.
2360 * If this value is found then it is appended to the begining of the id tag. If the grid is not found then the directive throws an error.
2361 * This is done in order to ensure uniqueness of id tags across the grid.
2362 * This is to prevent two grids in the same document having duplicate id tags.
2363 */
2364 {tag: 'Id', directiveName:'IdGrid', method: 'attr', appendGridId: true},
2365 /**
2366 * @ngdoc directive
2367 * @name ui.grid.directive:uiGridOneBindTitle
2368 * @element div
2369 * @restrict A
2370 * @param {String} uiGridOneBindTitle The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2371 * @description One time binding for the title dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2372 */
2373 {tag: 'Title', method: 'attr'},
2374 /**
2375 * @ngdoc directive
2376 * @name ui.grid.directive:uiGridOneBindAriaLabel
2377 * @element div
2378 * @restrict A
2379 * @param {String} uiGridOneBindAriaLabel The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2380 * @description One time binding for the aria-label dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2381 *<br/>
2382 * <pre>
2383 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text"></div>
2384 </pre>
2385 * Will become:
2386 * <pre>
2387 <div ng-init="text='Add this text'" ui-grid-one-bind-aria-label="text" aria-label="Add this text"></div>
2388 </pre>
2389 */
2390 {tag: 'Label', method: 'attr', aria:true},
2391 /**
2392 * @ngdoc directive
2393 * @name ui.grid.directive:uiGridOneBindAriaLabelledby
2394 * @element div
2395 * @restrict A
2396 * @param {String} uiGridOneBindAriaLabelledby The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2397 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2398 *<br/>
2399 * <pre>
2400 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId"></div>
2401 </pre>
2402 * Will become:
2403 * <pre>
2404 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby="anId" aria-labelledby="gridID32"></div>
2405 </pre>
2406 */
2407 {tag: 'Labelledby', method: 'attr', aria:true},
2408 /**
2409 * @ngdoc directive
2410 * @name ui.grid.directive:uiGridOneBindAriaLabelledbyGrid
2411 * @element div
2412 * @restrict A
2413 * @param {String} uiGridOneBindAriaLabelledbyGrid The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2414 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2415 * Works somewhat like {@link ui.grid.directive:uiGridOneBindIdGrid} however this one supports a list of ids (seperated by a space) and will dynamically add the
2416 * grid id to each one.
2417 *<br/>
2418 * <pre>
2419 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId"></div>
2420 </pre>
2421 * Will become ([grid.id] will be replaced by the actual grid id):
2422 * <pre>
2423 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-labelledby-grid="anId" aria-labelledby-Grid="[grid.id]-gridID32"></div>
2424 </pre>
2425 */
2426 {tag: 'Labelledby', directiveName:'LabelledbyGrid', appendGridId:true, method: 'attr', aria:true},
2427 /**
2428 * @ngdoc directive
2429 * @name ui.grid.directive:uiGridOneBindAriaDescribedby
2430 * @element ANY
2431 * @restrict A
2432 * @param {String} uiGridOneBindAriaDescribedby The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2433 * @description One time binding for the aria-describedby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2434 *<br/>
2435 * <pre>
2436 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId"></div>
2437 </pre>
2438 * Will become:
2439 * <pre>
2440 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby="anId" aria-describedby="gridID32"></div>
2441 </pre>
2442 */
2443 {tag: 'Describedby', method: 'attr', aria:true},
2444 /**
2445 * @ngdoc directive
2446 * @name ui.grid.directive:uiGridOneBindAriaDescribedbyGrid
2447 * @element ANY
2448 * @restrict A
2449 * @param {String} uiGridOneBindAriaDescribedbyGrid The angular string you want to bind. Does not support interpolation. Don't use <code>{{scopeElt}}</code> instead use <code>scopeElt</code>.
2450 * @description One time binding for the aria-labelledby dom tag. For more information see {@link ui.grid.directive:uiGridOneBind}.
2451 * Works somewhat like {@link ui.grid.directive:uiGridOneBindIdGrid} however this one supports a list of ids (seperated by a space) and will dynamically add the
2452 * grid id to each one.
2453 *<br/>
2454 * <pre>
2455 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId"></div>
2456 </pre>
2457 * Will become ([grid.id] will be replaced by the actual grid id):
2458 * <pre>
2459 <div ng-init="anId = 'gridID32'" ui-grid-one-bind-aria-describedby-grid="anId" aria-describedby="[grid.id]-gridID32"></div>
2460 </pre>
2461 */
2462 {tag: 'Describedby', directiveName:'DescribedbyGrid', appendGridId:true, method: 'attr', aria:true}],
2463 function(v){
2464
2465 var baseDirectiveName = 'uiGridOneBind';
2466 //If it is an aria tag then append the aria label seperately
2467 //This is done because the aria tags are formatted aria-* and the directive name can't have a '-' character in it.
2468 //If the diretiveName has to be overridden then it does so here. This is because the tag being modified and the directive sometimes don't match up.
2469 var directiveName = (v.aria ? baseDirectiveName + 'Aria' : baseDirectiveName) + (v.directiveName ? v.directiveName : v.tag);
2470 oneBinders.directive(directiveName, ['gridUtil', function(gridUtil){
2471 return {
2472 restrict: 'A',
2473 require: ['?uiGrid','?^uiGrid'],
2474 link: function(scope, iElement, iAttrs, controllers){
2475 /* Appends the grid id to the beginnig of the value. */
2476 var appendGridId = function(val){
2477 var grid; //Get an instance of the grid if its available
2478 //If its available in the scope then we don't need to try to find it elsewhere
2479 if (scope.grid) {
2480 grid = scope.grid;
2481 }
2482 //Another possible location to try to find the grid
2483 else if (scope.col && scope.col.grid){
2484 grid = scope.col.grid;
2485 }
2486 //Last ditch effort: Search through the provided controllers.
2487 else if (!controllers.some( //Go through the controllers till one has the element we need
2488 function(controller){
2489 if (controller && controller.grid) {
2490 grid = controller.grid;
2491 return true; //We've found the grid
2492 }
2493 })){
2494 //We tried our best to find it for you
2495 gridUtil.logError("["+directiveName+"] A valid grid could not be found to bind id. Are you using this directive " +
2496 "within the correct scope? Trying to generate id: [gridID]-" + val);
2497 throw new Error("No valid grid could be found");
2498 }
2499
2500 if (grid){
2501 var idRegex = new RegExp(grid.id.toString());
2502 //If the grid id hasn't been appended already in the template declaration
2503 if (!idRegex.test(val)){
2504 val = grid.id.toString() + '-' + val;
2505 }
2506 }
2507 return val;
2508 };
2509
2510 // The watch returns a function to remove itself.
2511 var rmWatcher = scope.$watch(iAttrs[directiveName], function(newV){
2512 if (newV){
2513 //If we are trying to add an id element then we also apply the grid id if it isn't already there
2514 if (v.appendGridId) {
2515 var newIdString = null;
2516 //Append the id to all of the new ids.
2517 angular.forEach( newV.split(' '), function(s){
2518 newIdString = (newIdString ? (newIdString + ' ') : '') + appendGridId(s);
2519 });
2520 newV = newIdString;
2521 }
2522
2523 // Append this newValue to the dom element.
2524 switch (v.method) {
2525 case 'attr': //The attr method takes two paraams the tag and the value
2526 if (v.aria) {
2527 //If it is an aria element then append the aria prefix
2528 iElement[v.method]('aria-' + v.tag.toLowerCase(),newV);
2529 } else {
2530 iElement[v.method](v.tag.toLowerCase(),newV);
2531 }
2532 break;
2533 case 'addClass':
2534 //Pulled from https://github.com/Pasvaz/bindonce/blob/master/bindonce.js
2535 if (angular.isObject(newV) && !angular.isArray(newV)) {
2536 var results = [];
2537 var nonNullFound = false; //We don't want to remove the binding unless the key is actually defined
2538 angular.forEach(newV, function (value, index) {
2539 if (value !== null && typeof(value) !== "undefined"){
2540 nonNullFound = true; //A non null value for a key was found so the object must have been initialized
2541 if (value) {results.push(index);}
2542 }
2543 });
2544 //A non null value for a key wasn't found so assume that the scope values haven't been fully initialized
2545 if (!nonNullFound){
2546 return; // If not initialized then the watcher should not be removed yet.
2547 }
2548 newV = results;
2549 }
2550
2551 if (newV) {
2552 iElement.addClass(angular.isArray(newV) ? newV.join(' ') : newV);
2553 } else {
2554 return;
2555 }
2556 break;
2557 default:
2558 iElement[v.method](newV);
2559 break;
2560 }
2561
2562 //Removes the watcher on itself after the bind
2563 rmWatcher();
2564 }
2565 // True ensures that equality is determined using angular.equals instead of ===
2566 }, true); //End rm watchers
2567 } //End compile function
2568 }; //End directive return
2569 } // End directive function
2570 ]); //End directive
2571 }); // End angular foreach
2572 })();
2573
2574 (function () {
2575 'use strict';
2576
2577 var module = angular.module('ui.grid');
2578
2579 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'uiGridConstants', 'gridUtil', 'ScrollEvent',
2580 function($timeout, $document, uiGridConstants, gridUtil, ScrollEvent) {
2581 return {
2582 replace: true,
2583 transclude: true,
2584 templateUrl: 'ui-grid/uiGridRenderContainer',
2585 require: ['^uiGrid', 'uiGridRenderContainer'],
2586 scope: {
2587 containerId: '=',
2588 rowContainerName: '=',
2589 colContainerName: '=',
2590 bindScrollHorizontal: '=',
2591 bindScrollVertical: '=',
2592 enableVerticalScrollbar: '=',
2593 enableHorizontalScrollbar: '='
2594 },
2595 controller: 'uiGridRenderContainer as RenderContainer',
2596 compile: function () {
2597 return {
2598 pre: function prelink($scope, $elm, $attrs, controllers) {
2599
2600 var uiGridCtrl = controllers[0];
2601 var containerCtrl = controllers[1];
2602 var grid = $scope.grid = uiGridCtrl.grid;
2603
2604 // Verify that the render container for this element exists
2605 if (!$scope.rowContainerName) {
2606 throw "No row render container name specified";
2607 }
2608 if (!$scope.colContainerName) {
2609 throw "No column render container name specified";
2610 }
2611
2612 if (!grid.renderContainers[$scope.rowContainerName]) {
2613 throw "Row render container '" + $scope.rowContainerName + "' is not registered.";
2614 }
2615 if (!grid.renderContainers[$scope.colContainerName]) {
2616 throw "Column render container '" + $scope.colContainerName + "' is not registered.";
2617 }
2618
2619 var rowContainer = $scope.rowContainer = grid.renderContainers[$scope.rowContainerName];
2620 var colContainer = $scope.colContainer = grid.renderContainers[$scope.colContainerName];
2621
2622 containerCtrl.containerId = $scope.containerId;
2623 containerCtrl.rowContainer = rowContainer;
2624 containerCtrl.colContainer = colContainer;
2625 },
2626 post: function postlink($scope, $elm, $attrs, controllers) {
2627
2628 var uiGridCtrl = controllers[0];
2629 var containerCtrl = controllers[1];
2630
2631 var grid = uiGridCtrl.grid;
2632 var rowContainer = containerCtrl.rowContainer;
2633 var colContainer = containerCtrl.colContainer;
2634 var scrollTop = null;
2635 var scrollLeft = null;
2636
2637
2638 var renderContainer = grid.renderContainers[$scope.containerId];
2639
2640 // Put the container name on this element as a class
2641 $elm.addClass('ui-grid-render-container-' + $scope.containerId);
2642
2643 // Scroll the render container viewport when the mousewheel is used
2644 gridUtil.on.mousewheel($elm, function (event) {
2645 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.RenderContainerMouseWheel);
2646 if (event.deltaY !== 0) {
2647 var scrollYAmount = event.deltaY * -1 * event.deltaFactor;
2648
2649 scrollTop = containerCtrl.viewport[0].scrollTop;
2650
2651 // Get the scroll percentage
2652 scrollEvent.verticalScrollLength = rowContainer.getVerticalScrollLength();
2653 var scrollYPercentage = (scrollTop + scrollYAmount) / scrollEvent.verticalScrollLength;
2654
2655 // If we should be scrolled 100%, make sure the scrollTop matches the maximum scroll length
2656 // Viewports that have "overflow: hidden" don't let the mousewheel scroll all the way to the bottom without this check
2657 if (scrollYPercentage >= 1 && scrollTop < scrollEvent.verticalScrollLength) {
2658 containerCtrl.viewport[0].scrollTop = scrollEvent.verticalScrollLength;
2659 }
2660
2661 // Keep scrollPercentage within the range 0-1.
2662 if (scrollYPercentage < 0) { scrollYPercentage = 0; }
2663 else if (scrollYPercentage > 1) { scrollYPercentage = 1; }
2664
2665 scrollEvent.y = { percentage: scrollYPercentage, pixels: scrollYAmount };
2666 }
2667 if (event.deltaX !== 0) {
2668 var scrollXAmount = event.deltaX * event.deltaFactor;
2669
2670 // Get the scroll percentage
2671 scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport, grid);
2672 scrollEvent.horizontalScrollLength = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
2673 var scrollXPercentage = (scrollLeft + scrollXAmount) / scrollEvent.horizontalScrollLength;
2674
2675 // Keep scrollPercentage within the range 0-1.
2676 if (scrollXPercentage < 0) { scrollXPercentage = 0; }
2677 else if (scrollXPercentage > 1) { scrollXPercentage = 1; }
2678
2679 scrollEvent.x = { percentage: scrollXPercentage, pixels: scrollXAmount };
2680 }
2681
2682 // Let the parent container scroll if the grid is already at the top/bottom
2683 if ((event.deltaY !== 0 && (scrollEvent.atTop(scrollTop) || scrollEvent.atBottom(scrollTop))) ||
2684 (event.deltaX !== 0 && (scrollEvent.atLeft(scrollLeft) || scrollEvent.atRight(scrollLeft)))) {
2685 //parent controller scrolls
2686 }
2687 else {
2688 event.preventDefault();
2689 event.stopPropagation();
2690 scrollEvent.fireThrottledScrollingEvent('', scrollEvent);
2691 }
2692
2693 });
2694
2695 $elm.bind('$destroy', function() {
2696 $elm.unbind('keydown');
2697
2698 ['touchstart', 'touchmove', 'touchend','keydown', 'wheel', 'mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'].forEach(function (eventName) {
2699 $elm.unbind(eventName);
2700 });
2701 });
2702
2703 // TODO(c0bra): Handle resizing the inner canvas based on the number of elements
2704 function update() {
2705 var ret = '';
2706
2707 var canvasWidth = colContainer.canvasWidth;
2708 var viewportWidth = colContainer.getViewportWidth();
2709
2710 var canvasHeight = rowContainer.getCanvasHeight();
2711
2712 //add additional height for scrollbar on left and right container
2713 //if ($scope.containerId !== 'body') {
2714 // canvasHeight -= grid.scrollbarHeight;
2715 //}
2716
2717 var viewportHeight = rowContainer.getViewportHeight();
2718 //shorten the height to make room for a scrollbar placeholder
2719 if (colContainer.needsHScrollbarPlaceholder()) {
2720 viewportHeight -= grid.scrollbarHeight;
2721 }
2722
2723 var headerViewportWidth,
2724 footerViewportWidth;
2725 headerViewportWidth = footerViewportWidth = colContainer.getHeaderViewportWidth();
2726
2727 // Set canvas dimensions
2728 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-canvas { width: ' + canvasWidth + 'px; height: ' + canvasHeight + 'px; }';
2729
2730 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2731
2732 if (renderContainer.explicitHeaderCanvasHeight) {
2733 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: ' + renderContainer.explicitHeaderCanvasHeight + 'px; }';
2734 }
2735 else {
2736 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: inherit; }';
2737 }
2738
2739 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }';
2740 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }';
2741
2742 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }';
2743 ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-viewport { width: ' + footerViewportWidth + 'px; }';
2744
2745 return ret;
2746 }
2747
2748 uiGridCtrl.grid.registerStyleComputation({
2749 priority: 6,
2750 func: update
2751 });
2752 }
2753 };
2754 }
2755 };
2756
2757 }]);
2758
2759 module.controller('uiGridRenderContainer', ['$scope', 'gridUtil', function ($scope, gridUtil) {
2760
2761 }]);
2762
2763 })();
2764
2765 (function(){
2766 'use strict';
2767
2768 angular.module('ui.grid').directive('uiGridRow', ['gridUtil', function(gridUtil) {
2769 return {
2770 replace: true,
2771 // priority: 2001,
2772 // templateUrl: 'ui-grid/ui-grid-row',
2773 require: ['^uiGrid', '^uiGridRenderContainer'],
2774 scope: {
2775 row: '=uiGridRow',
2776 //rowRenderIndex is added to scope to give the true visual index of the row to any directives that need it
2777 rowRenderIndex: '='
2778 },
2779 compile: function() {
2780 return {
2781 pre: function($scope, $elm, $attrs, controllers) {
2782 var uiGridCtrl = controllers[0];
2783 var containerCtrl = controllers[1];
2784
2785 var grid = uiGridCtrl.grid;
2786
2787 $scope.grid = uiGridCtrl.grid;
2788 $scope.colContainer = containerCtrl.colContainer;
2789
2790 // Function for attaching the template to this scope
2791 var clonedElement, cloneScope;
2792 function compileTemplate() {
2793 $scope.row.getRowTemplateFn.then(function (compiledElementFn) {
2794 // var compiledElementFn = $scope.row.compiledElementFn;
2795
2796 // Create a new scope for the contents of this row, so we can destroy it later if need be
2797 var newScope = $scope.$new();
2798
2799 compiledElementFn(newScope, function (newElm, scope) {
2800 // If we already have a cloned element, we need to remove it and destroy its scope
2801 if (clonedElement) {
2802 clonedElement.remove();
2803 cloneScope.$destroy();
2804 }
2805
2806 // Empty the row and append the new element
2807 $elm.empty().append(newElm);
2808
2809 // Save the new cloned element and scope
2810 clonedElement = newElm;
2811 cloneScope = newScope;
2812 });
2813 });
2814 }
2815
2816 // Initially attach the compiled template to this scope
2817 compileTemplate();
2818
2819 // If the row's compiled element function changes, we need to replace this element's contents with the new compiled template
2820 $scope.$watch('row.getRowTemplateFn', function (newFunc, oldFunc) {
2821 if (newFunc !== oldFunc) {
2822 compileTemplate();
2823 }
2824 });
2825 },
2826 post: function($scope, $elm, $attrs, controllers) {
2827
2828 }
2829 };
2830 }
2831 };
2832 }]);
2833
2834 })();
2835 (function(){
2836 // 'use strict';
2837
2838 /**
2839 * @ngdoc directive
2840 * @name ui.grid.directive:uiGridStyle
2841 * @element style
2842 * @restrict A
2843 *
2844 * @description
2845 * Allows us to interpolate expressions in `<style>` elements. Angular doesn't do this by default as it can/will/might? break in IE8.
2846 *
2847 * @example
2848 <doc:example module="app">
2849 <doc:source>
2850 <script>
2851 var app = angular.module('app', ['ui.grid']);
2852
2853 app.controller('MainCtrl', ['$scope', function ($scope) {
2854 $scope.myStyle = '.blah { border: 1px solid }';
2855 }]);
2856 </script>
2857
2858 <div ng-controller="MainCtrl">
2859 <style ui-grid-style>{{ myStyle }}</style>
2860 <span class="blah">I am in a box.</span>
2861 </div>
2862 </doc:source>
2863 <doc:scenario>
2864 it('should apply the right class to the element', function () {
2865 element(by.css('.blah')).getCssValue('border-top-width')
2866 .then(function(c) {
2867 expect(c).toContain('1px');
2868 });
2869 });
2870 </doc:scenario>
2871 </doc:example>
2872 */
2873
2874
2875 angular.module('ui.grid').directive('uiGridStyle', ['gridUtil', '$interpolate', function(gridUtil, $interpolate) {
2876 return {
2877 // restrict: 'A',
2878 // priority: 1000,
2879 // require: '?^uiGrid',
2880 link: function($scope, $elm, $attrs, uiGridCtrl) {
2881 // gridUtil.logDebug('ui-grid-style link');
2882 // if (uiGridCtrl === undefined) {
2883 // gridUtil.logWarn('[ui-grid-style link] uiGridCtrl is undefined!');
2884 // }
2885
2886 var interpolateFn = $interpolate($elm.text(), true);
2887
2888 if (interpolateFn) {
2889 $scope.$watch(interpolateFn, function(value) {
2890 $elm.text(value);
2891 });
2892 }
2893
2894 // uiGridCtrl.recalcRowStyles = function() {
2895 // var offset = (scope.options.offsetTop || 0) - (scope.options.excessRows * scope.options.rowHeight);
2896 // var rowHeight = scope.options.rowHeight;
2897
2898 // var ret = '';
2899 // var rowStyleCount = uiGridCtrl.minRowsToRender() + (scope.options.excessRows * 2);
2900 // for (var i = 1; i <= rowStyleCount; i++) {
2901 // ret = ret + ' .grid' + scope.gridId + ' .ui-grid-row:nth-child(' + i + ') { top: ' + offset + 'px; }';
2902 // offset = offset + rowHeight;
2903 // }
2904
2905 // scope.rowStyles = ret;
2906 // };
2907
2908 // uiGridCtrl.styleComputions.push(uiGridCtrl.recalcRowStyles);
2909
2910 }
2911 };
2912 }]);
2913
2914 })();
2915
2916 (function(){
2917 'use strict';
2918
2919 angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', '$log',
2920 function(gridUtil, ScrollEvent, uiGridConstants, $log) {
2921 return {
2922 replace: true,
2923 scope: {},
2924 controllerAs: 'Viewport',
2925 templateUrl: 'ui-grid/uiGridViewport',
2926 require: ['^uiGrid', '^uiGridRenderContainer'],
2927 link: function($scope, $elm, $attrs, controllers) {
2928 // gridUtil.logDebug('viewport post-link');
2929
2930 var uiGridCtrl = controllers[0];
2931 var containerCtrl = controllers[1];
2932
2933 $scope.containerCtrl = containerCtrl;
2934
2935 var rowContainer = containerCtrl.rowContainer;
2936 var colContainer = containerCtrl.colContainer;
2937
2938 var grid = uiGridCtrl.grid;
2939
2940 $scope.grid = uiGridCtrl.grid;
2941
2942 // Put the containers in scope so we can get rows and columns from them
2943 $scope.rowContainer = containerCtrl.rowContainer;
2944 $scope.colContainer = containerCtrl.colContainer;
2945
2946 // Register this viewport with its container
2947 containerCtrl.viewport = $elm;
2948
2949
2950 $elm.on('scroll', scrollHandler);
2951
2952 var ignoreScroll = false;
2953
2954 function scrollHandler(evt) {
2955 //Leaving in this commented code in case it can someday be used
2956 //It does improve performance, but because the horizontal scroll is normalized,
2957 // using this code will lead to the column header getting slightly out of line with columns
2958 //
2959 //if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) {
2960 // //don't ask for scrollTop if we just set it
2961 // ignoreScroll = false;
2962 // return;
2963 //}
2964 //ignoreScroll = true;
2965
2966 var newScrollTop = $elm[0].scrollTop;
2967 var newScrollLeft = gridUtil.normalizeScrollLeft($elm, grid);
2968
2969 var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop);
2970 var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft);
2971
2972 var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll);
2973 scrollEvent.newScrollLeft = newScrollLeft;
2974 scrollEvent.newScrollTop = newScrollTop;
2975 if ( horizScrollPercentage > -1 ){
2976 scrollEvent.x = { percentage: horizScrollPercentage };
2977 }
2978
2979 if ( vertScrollPercentage > -1 ){
2980 scrollEvent.y = { percentage: vertScrollPercentage };
2981 }
2982
2983 grid.scrollContainers($scope.$parent.containerId, scrollEvent);
2984 }
2985
2986 if ($scope.$parent.bindScrollVertical) {
2987 grid.addVerticalScrollSync($scope.$parent.containerId, syncVerticalScroll);
2988 }
2989
2990 if ($scope.$parent.bindScrollHorizontal) {
2991 grid.addHorizontalScrollSync($scope.$parent.containerId, syncHorizontalScroll);
2992 grid.addHorizontalScrollSync($scope.$parent.containerId + 'header', syncHorizontalHeader);
2993 grid.addHorizontalScrollSync($scope.$parent.containerId + 'footer', syncHorizontalFooter);
2994 }
2995
2996 function syncVerticalScroll(scrollEvent){
2997 containerCtrl.prevScrollArgs = scrollEvent;
2998 var newScrollTop = scrollEvent.getNewScrollTop(rowContainer,containerCtrl.viewport);
2999 $elm[0].scrollTop = newScrollTop;
3000
3001 }
3002
3003 function syncHorizontalScroll(scrollEvent){
3004 containerCtrl.prevScrollArgs = scrollEvent;
3005 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3006 $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3007 }
3008
3009 function syncHorizontalHeader(scrollEvent){
3010 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3011 if (containerCtrl.headerViewport) {
3012 containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3013 }
3014 }
3015
3016 function syncHorizontalFooter(scrollEvent){
3017 var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport);
3018 if (containerCtrl.footerViewport) {
3019 containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid);
3020 }
3021 }
3022
3023
3024 },
3025 controller: ['$scope', function ($scope) {
3026 this.rowStyle = function (index) {
3027 var rowContainer = $scope.rowContainer;
3028 var colContainer = $scope.colContainer;
3029
3030 var styles = {};
3031
3032 if (index === 0 && rowContainer.currentTopRow !== 0) {
3033 // The row offset-top is just the height of the rows above the current top-most row, which are no longer rendered
3034 var hiddenRowWidth = (rowContainer.currentTopRow) * rowContainer.grid.options.rowHeight;
3035
3036 // return { 'margin-top': hiddenRowWidth + 'px' };
3037 styles['margin-top'] = hiddenRowWidth + 'px';
3038 }
3039
3040 if (colContainer.currentFirstColumn !== 0) {
3041 if (colContainer.grid.isRTL()) {
3042 styles['margin-right'] = colContainer.columnOffset + 'px';
3043 }
3044 else {
3045 styles['margin-left'] = colContainer.columnOffset + 'px';
3046 }
3047 }
3048
3049 return styles;
3050 };
3051 }]
3052 };
3053 }
3054 ]);
3055
3056 })();
3057
3058 (function() {
3059
3060 angular.module('ui.grid')
3061 .directive('uiGridVisible', function uiGridVisibleAction() {
3062 return function ($scope, $elm, $attr) {
3063 $scope.$watch($attr.uiGridVisible, function (visible) {
3064 // $elm.css('visibility', visible ? 'visible' : 'hidden');
3065 $elm[visible ? 'removeClass' : 'addClass']('ui-grid-invisible');
3066 });
3067 };
3068 });
3069
3070 })();
3071 (function () {
3072 'use strict';
3073
3074 angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants',
3075 '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile',
3076 function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants,
3077 $templateCache, gridClassFactory, $timeout, $parse, $compile) {
3078 // gridUtil.logDebug('ui-grid controller');
3079
3080 var self = this;
3081
3082 self.grid = gridClassFactory.createGrid($scope.uiGrid);
3083
3084 //assign $scope.$parent if appScope not already assigned
3085 self.grid.appScope = self.grid.appScope || $scope.$parent;
3086
3087 $elm.addClass('grid' + self.grid.id);
3088 self.grid.rtl = gridUtil.getStyles($elm[0])['direction'] === 'rtl';
3089
3090
3091 // angular.extend(self.grid.options, );
3092
3093 //all properties of grid are available on scope
3094 $scope.grid = self.grid;
3095
3096 if ($attrs.uiGridColumns) {
3097 $attrs.$observe('uiGridColumns', function(value) {
3098 self.grid.options.columnDefs = value;
3099 self.grid.buildColumns()
3100 .then(function(){
3101 self.grid.preCompileCellTemplates();
3102
3103 self.grid.refreshCanvas(true);
3104 });
3105 });
3106 }
3107
3108
3109 // if fastWatch is set we watch only the length and the reference, not every individual object
3110 var deregFunctions = [];
3111 if (self.grid.options.fastWatch) {
3112 self.uiGrid = $scope.uiGrid;
3113 if (angular.isString($scope.uiGrid.data)) {
3114 deregFunctions.push( $scope.$parent.$watch($scope.uiGrid.data, dataWatchFunction) );
3115 deregFunctions.push( $scope.$parent.$watch(function() {
3116 if ( self.grid.appScope[$scope.uiGrid.data] ){
3117 return self.grid.appScope[$scope.uiGrid.data].length;
3118 } else {
3119 return undefined;
3120 }
3121 }, dataWatchFunction) );
3122 } else {
3123 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3124 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data.length; }, dataWatchFunction) );
3125 }
3126 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3127 deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs.length; }, columnDefsWatchFunction) );
3128 } else {
3129 if (angular.isString($scope.uiGrid.data)) {
3130 deregFunctions.push( $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction) );
3131 } else {
3132 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction) );
3133 }
3134 deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) );
3135 }
3136
3137
3138 function columnDefsWatchFunction(n, o) {
3139 if (n && n !== o) {
3140 self.grid.options.columnDefs = n;
3141 self.grid.buildColumns({ orderByColumnDefs: true })
3142 .then(function(){
3143
3144 self.grid.preCompileCellTemplates();
3145
3146 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.COLUMN);
3147 });
3148 }
3149 }
3150
3151 function dataWatchFunction(newData) {
3152 // gridUtil.logDebug('dataWatch fired');
3153 var promises = [];
3154
3155 if ( self.grid.options.fastWatch ){
3156 if (angular.isString($scope.uiGrid.data)) {
3157 newData = self.grid.appScope[$scope.uiGrid.data];
3158 } else {
3159 newData = $scope.uiGrid.data;
3160 }
3161 }
3162
3163 if (newData) {
3164 // columns length is greater than the number of row header columns, which don't count because they're created automatically
3165 var hasColumns = self.grid.columns.length > (self.grid.rowHeaderColumns ? self.grid.rowHeaderColumns.length : 0);
3166
3167 if (
3168 // If we have no columns
3169 !hasColumns &&
3170 // ... and we don't have a ui-grid-columns attribute, which would define columns for us
3171 !$attrs.uiGridColumns &&
3172 // ... and we have no pre-defined columns
3173 self.grid.options.columnDefs.length === 0 &&
3174 // ... but we DO have data
3175 newData.length > 0
3176 ) {
3177 // ... then build the column definitions from the data that we have
3178 self.grid.buildColumnDefsFromData(newData);
3179 }
3180
3181 // If we haven't built columns before and either have some columns defined or some data defined
3182 if (!hasColumns && (self.grid.options.columnDefs.length > 0 || newData.length > 0)) {
3183 // Build the column set, then pre-compile the column cell templates
3184 promises.push(self.grid.buildColumns()
3185 .then(function() {
3186 self.grid.preCompileCellTemplates();
3187 }));
3188 }
3189
3190 $q.all(promises).then(function() {
3191 self.grid.modifyRows(newData)
3192 .then(function () {
3193 // if (self.viewport) {
3194 self.grid.redrawInPlace(true);
3195 // }
3196
3197 $scope.$evalAsync(function() {
3198 self.grid.refreshCanvas(true);
3199 self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW);
3200 });
3201 });
3202 });
3203 }
3204 }
3205
3206 var styleWatchDereg = $scope.$watch(function () { return self.grid.styleComputations; }, function() {
3207 self.grid.refreshCanvas(true);
3208 });
3209
3210 $scope.$on('$destroy', function() {
3211 deregFunctions.forEach( function( deregFn ){ deregFn(); });
3212 styleWatchDereg();
3213 });
3214
3215 self.fireEvent = function(eventName, args) {
3216 // Add the grid to the event arguments if it's not there
3217 if (typeof(args) === 'undefined' || args === undefined) {
3218 args = {};
3219 }
3220
3221 if (typeof(args.grid) === 'undefined' || args.grid === undefined) {
3222 args.grid = self.grid;
3223 }
3224
3225 $scope.$broadcast(eventName, args);
3226 };
3227
3228 self.innerCompile = function innerCompile(elm) {
3229 $compile(elm)($scope);
3230 };
3231
3232 }]);
3233
3234 /**
3235 * @ngdoc directive
3236 * @name ui.grid.directive:uiGrid
3237 * @element div
3238 * @restrict EA
3239 * @param {Object} uiGrid Options for the grid to use
3240 *
3241 * @description Create a very basic grid.
3242 *
3243 * @example
3244 <example module="app">
3245 <file name="app.js">
3246 var app = angular.module('app', ['ui.grid']);
3247
3248 app.controller('MainCtrl', ['$scope', function ($scope) {
3249 $scope.data = [
3250 { name: 'Bob', title: 'CEO' },
3251 { name: 'Frank', title: 'Lowly Developer' }
3252 ];
3253 }]);
3254 </file>
3255 <file name="index.html">
3256 <div ng-controller="MainCtrl">
3257 <div ui-grid="{ data: data }"></div>
3258 </div>
3259 </file>
3260 </example>
3261 */
3262 angular.module('ui.grid').directive('uiGrid', uiGridDirective);
3263
3264 uiGridDirective.$inject = ['$compile', '$templateCache', '$timeout', '$window', 'gridUtil', 'uiGridConstants'];
3265 function uiGridDirective($compile, $templateCache, $timeout, $window, gridUtil, uiGridConstants) {
3266 return {
3267 templateUrl: 'ui-grid/ui-grid',
3268 scope: {
3269 uiGrid: '='
3270 },
3271 replace: true,
3272 transclude: true,
3273 controller: 'uiGridController',
3274 compile: function () {
3275 return {
3276 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3277 var grid = uiGridCtrl.grid;
3278 // Initialize scrollbars (TODO: move to controller??)
3279 uiGridCtrl.scrollbars = [];
3280 grid.element = $elm;
3281
3282
3283 // See if the grid has a rendered width, if not, wait a bit and try again
3284 var sizeCheckInterval = 100; // ms
3285 var maxSizeChecks = 20; // 2 seconds total
3286 var sizeChecks = 0;
3287
3288 // Setup (event listeners) the grid
3289 setup();
3290
3291 // And initialize it
3292 init();
3293
3294 // Mark rendering complete so API events can happen
3295 grid.renderingComplete();
3296
3297 // If the grid doesn't have size currently, wait for a bit to see if it gets size
3298 checkSize();
3299
3300 /*-- Methods --*/
3301
3302 function checkSize() {
3303 // If the grid has no width and we haven't checked more than <maxSizeChecks> times, check again in <sizeCheckInterval> milliseconds
3304 if ($elm[0].offsetWidth <= 0 && sizeChecks < maxSizeChecks) {
3305 setTimeout(checkSize, sizeCheckInterval);
3306 sizeChecks++;
3307 }
3308 else {
3309 $timeout(init);
3310 }
3311 }
3312
3313 // Setup event listeners and watchers
3314 function setup() {
3315 // Bind to window resize events
3316 angular.element($window).on('resize', gridResize);
3317
3318 // Unbind from window resize events when the grid is destroyed
3319 $elm.on('$destroy', function () {
3320 angular.element($window).off('resize', gridResize);
3321 });
3322
3323 // If we add a left container after render, we need to watch and react
3324 $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) {
3325 if (newValue === oldValue) {
3326 return;
3327 }
3328 grid.refreshCanvas(true);
3329 });
3330
3331 // If we add a right container after render, we need to watch and react
3332 $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) {
3333 if (newValue === oldValue) {
3334 return;
3335 }
3336 grid.refreshCanvas(true);
3337 });
3338 }
3339
3340 // Initialize the directive
3341 function init() {
3342 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3343
3344 // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from
3345 grid.canvasWidth = uiGridCtrl.grid.gridWidth;
3346
3347 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3348
3349 // If the grid isn't tall enough to fit a single row, it's kind of useless. Resize it to fit a minimum number of rows
3350 if (grid.gridHeight < grid.options.rowHeight && grid.options.enableMinHeightCheck) {
3351 autoAdjustHeight();
3352 }
3353
3354 // Run initial canvas refresh
3355 grid.refreshCanvas(true);
3356 }
3357
3358 // Set the grid's height ourselves in the case that its height would be unusably small
3359 function autoAdjustHeight() {
3360 // Figure out the new height
3361 var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight;
3362 var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0;
3363 var footerHeight = grid.calcFooterHeight();
3364
3365 var scrollbarHeight = 0;
3366 if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3367 scrollbarHeight = gridUtil.getScrollbarWidth();
3368 }
3369
3370 var maxNumberOfFilters = 0;
3371 // Calculates the maximum number of filters in the columns
3372 angular.forEach(grid.options.columnDefs, function(col) {
3373 if (col.hasOwnProperty('filter')) {
3374 if (maxNumberOfFilters < 1) {
3375 maxNumberOfFilters = 1;
3376 }
3377 }
3378 else if (col.hasOwnProperty('filters')) {
3379 if (maxNumberOfFilters < col.filters.length) {
3380 maxNumberOfFilters = col.filters.length;
3381 }
3382 }
3383 });
3384
3385 if (grid.options.enableFiltering) {
3386 var allColumnsHaveFilteringTurnedOff = grid.options.columnDefs.every(function(col) {
3387 return col.enableFiltering === false;
3388 });
3389
3390 if (!allColumnsHaveFilteringTurnedOff) {
3391 maxNumberOfFilters++;
3392 }
3393 }
3394
3395 var filterHeight = maxNumberOfFilters * headerHeight;
3396
3397 var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight;
3398
3399 $elm.css('height', newHeight + 'px');
3400
3401 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3402 }
3403
3404 // Resize the grid on window resize events
3405 function gridResize($event) {
3406 grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm);
3407 grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm);
3408
3409 grid.refreshCanvas(true);
3410 }
3411 }
3412 };
3413 }
3414 };
3415 }
3416
3417 })();
3418
3419 (function(){
3420 'use strict';
3421
3422 // TODO: rename this file to ui-grid-pinned-container.js
3423
3424 angular.module('ui.grid').directive('uiGridPinnedContainer', ['gridUtil', function (gridUtil) {
3425 return {
3426 restrict: 'EA',
3427 replace: true,
3428 template: '<div class="ui-grid-pinned-container"><div ui-grid-render-container container-id="side" row-container-name="\'body\'" col-container-name="side" bind-scroll-vertical="true" class="{{ side }} ui-grid-render-container-{{ side }}"></div></div>',
3429 scope: {
3430 side: '=uiGridPinnedContainer'
3431 },
3432 require: '^uiGrid',
3433 compile: function compile() {
3434 return {
3435 post: function ($scope, $elm, $attrs, uiGridCtrl) {
3436 // gridUtil.logDebug('ui-grid-pinned-container ' + $scope.side + ' link');
3437
3438 var grid = uiGridCtrl.grid;
3439
3440 var myWidth = 0;
3441
3442 $elm.addClass('ui-grid-pinned-container-' + $scope.side);
3443
3444 // Monkey-patch the viewport width function
3445 if ($scope.side === 'left' || $scope.side === 'right') {
3446 grid.renderContainers[$scope.side].getViewportWidth = monkeyPatchedGetViewportWidth;
3447 }
3448
3449 function monkeyPatchedGetViewportWidth() {
3450 /*jshint validthis: true */
3451 var self = this;
3452
3453 var viewportWidth = 0;
3454 self.visibleColumnCache.forEach(function (column) {
3455 viewportWidth += column.drawnWidth;
3456 });
3457
3458 var adjustment = self.getViewportAdjustment();
3459
3460 viewportWidth = viewportWidth + adjustment.width;
3461
3462 return viewportWidth;
3463 }
3464
3465 function updateContainerWidth() {
3466 if ($scope.side === 'left' || $scope.side === 'right') {
3467 var cols = grid.renderContainers[$scope.side].visibleColumnCache;
3468 var width = 0;
3469 for (var i = 0; i < cols.length; i++) {
3470 var col = cols[i];
3471 width += col.drawnWidth || col.width || 0;
3472 }
3473
3474 return width;
3475 }
3476 }
3477
3478 function updateContainerDimensions() {
3479 var ret = '';
3480
3481 // Column containers
3482 if ($scope.side === 'left' || $scope.side === 'right') {
3483 myWidth = updateContainerWidth();
3484
3485 // gridUtil.logDebug('myWidth', myWidth);
3486
3487 // TODO(c0bra): Subtract sum of col widths from grid viewport width and update it
3488 $elm.attr('style', null);
3489
3490 // var myHeight = grid.renderContainers.body.getViewportHeight(); // + grid.horizontalScrollbarHeight;
3491
3492 ret += '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ', .grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.side + ' .ui-grid-render-container-' + $scope.side + ' .ui-grid-viewport { width: ' + myWidth + 'px; } ';
3493 }
3494
3495 return ret;
3496 }
3497
3498 grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
3499 myWidth = updateContainerWidth();
3500
3501 // Subtract our own width
3502 adjustment.width -= myWidth;
3503 adjustment.side = $scope.side;
3504
3505 return adjustment;
3506 });
3507
3508 // Register style computation to adjust for columns in `side`'s render container
3509 grid.registerStyleComputation({
3510 priority: 15,
3511 func: updateContainerDimensions
3512 });
3513 }
3514 };
3515 }
3516 };
3517 }]);
3518 })();
3519
3520 (function(){
3521
3522 angular.module('ui.grid')
3523 .factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout','ScrollEvent',
3524 function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout, ScrollEvent) {
3525
3526 /**
3527 * @ngdoc object
3528 * @name ui.grid.core.api:PublicApi
3529 * @description Public Api for the core grid features
3530 *
3531 */
3532
3533 /**
3534 * @ngdoc function
3535 * @name ui.grid.class:Grid
3536 * @description Grid is the main viewModel. Any properties or methods needed to maintain state are defined in
3537 * this prototype. One instance of Grid is created per Grid directive instance.
3538 * @param {object} options Object map of options to pass into the grid. An 'id' property is expected.
3539 */
3540 var Grid = function Grid(options) {
3541 var self = this;
3542 // Get the id out of the options, then remove it
3543 if (options !== undefined && typeof(options.id) !== 'undefined' && options.id) {
3544 if (!/^[_a-zA-Z0-9-]+$/.test(options.id)) {
3545 throw new Error("Grid id '" + options.id + '" is invalid. It must follow CSS selector syntax rules.');
3546 }
3547 }
3548 else {
3549 throw new Error('No ID provided. An ID must be given when creating a grid.');
3550 }
3551
3552 self.id = options.id;
3553 delete options.id;
3554
3555 // Get default options
3556 self.options = GridOptions.initialize( options );
3557
3558 /**
3559 * @ngdoc object
3560 * @name appScope
3561 * @propertyOf ui.grid.class:Grid
3562 * @description reference to the application scope (the parent scope of the ui-grid element). Assigned in ui-grid controller
3563 * <br/>
3564 * use gridOptions.appScopeProvider to override the default assignment of $scope.$parent with any reference
3565 */
3566 self.appScope = self.options.appScopeProvider;
3567
3568 self.headerHeight = self.options.headerRowHeight;
3569
3570
3571 /**
3572 * @ngdoc object
3573 * @name footerHeight
3574 * @propertyOf ui.grid.class:Grid
3575 * @description returns the total footer height gridFooter + columnFooter
3576 */
3577 self.footerHeight = self.calcFooterHeight();
3578
3579
3580 /**
3581 * @ngdoc object
3582 * @name columnFooterHeight
3583 * @propertyOf ui.grid.class:Grid
3584 * @description returns the total column footer height
3585 */
3586 self.columnFooterHeight = self.calcColumnFooterHeight();
3587
3588 self.rtl = false;
3589 self.gridHeight = 0;
3590 self.gridWidth = 0;
3591 self.columnBuilders = [];
3592 self.rowBuilders = [];
3593 self.rowsProcessors = [];
3594 self.columnsProcessors = [];
3595 self.styleComputations = [];
3596 self.viewportAdjusters = [];
3597 self.rowHeaderColumns = [];
3598 self.dataChangeCallbacks = {};
3599 self.verticalScrollSyncCallBackFns = {};
3600 self.horizontalScrollSyncCallBackFns = {};
3601
3602 // self.visibleRowCache = [];
3603
3604 // Set of 'render' containers for self grid, which can render sets of rows
3605 self.renderContainers = {};
3606
3607 // Create a
3608 self.renderContainers.body = new GridRenderContainer('body', self);
3609
3610 self.cellValueGetterCache = {};
3611
3612 // Cached function to use with custom row templates
3613 self.getRowTemplateFn = null;
3614
3615
3616 //representation of the rows on the grid.
3617 //these are wrapped references to the actual data rows (options.data)
3618 self.rows = [];
3619
3620 //represents the columns on the grid
3621 self.columns = [];
3622
3623 /**
3624 * @ngdoc boolean
3625 * @name isScrollingVertically
3626 * @propertyOf ui.grid.class:Grid
3627 * @description set to true when Grid is scrolling vertically. Set to false via debounced method
3628 */
3629 self.isScrollingVertically = false;
3630
3631 /**
3632 * @ngdoc boolean
3633 * @name isScrollingHorizontally
3634 * @propertyOf ui.grid.class:Grid
3635 * @description set to true when Grid is scrolling horizontally. Set to false via debounced method
3636 */
3637 self.isScrollingHorizontally = false;
3638
3639 /**
3640 * @ngdoc property
3641 * @name scrollDirection
3642 * @propertyOf ui.grid.class:Grid
3643 * @description set one of the uiGridConstants.scrollDirection values (UP, DOWN, LEFT, RIGHT, NONE), which tells
3644 * us which direction we are scrolling. Set to NONE via debounced method
3645 */
3646 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3647
3648 //if true, grid will not respond to any scroll events
3649 self.disableScrolling = false;
3650
3651
3652 function vertical (scrollEvent) {
3653 self.isScrollingVertically = false;
3654 self.api.core.raise.scrollEnd(scrollEvent);
3655 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3656 }
3657
3658 var debouncedVertical = gridUtil.debounce(vertical, self.options.scrollDebounce);
3659 var debouncedVerticalMinDelay = gridUtil.debounce(vertical, 0);
3660
3661 function horizontal (scrollEvent) {
3662 self.isScrollingHorizontally = false;
3663 self.api.core.raise.scrollEnd(scrollEvent);
3664 self.scrollDirection = uiGridConstants.scrollDirection.NONE;
3665 }
3666
3667 var debouncedHorizontal = gridUtil.debounce(horizontal, self.options.scrollDebounce);
3668 var debouncedHorizontalMinDelay = gridUtil.debounce(horizontal, 0);
3669
3670
3671 /**
3672 * @ngdoc function
3673 * @name flagScrollingVertically
3674 * @methodOf ui.grid.class:Grid
3675 * @description sets isScrollingVertically to true and sets it to false in a debounced function
3676 */
3677 self.flagScrollingVertically = function(scrollEvent) {
3678 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3679 self.api.core.raise.scrollBegin(scrollEvent);
3680 }
3681 self.isScrollingVertically = true;
3682 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3683 debouncedVerticalMinDelay(scrollEvent);
3684 }
3685 else {
3686 debouncedVertical(scrollEvent);
3687 }
3688 };
3689
3690 /**
3691 * @ngdoc function
3692 * @name flagScrollingHorizontally
3693 * @methodOf ui.grid.class:Grid
3694 * @description sets isScrollingHorizontally to true and sets it to false in a debounced function
3695 */
3696 self.flagScrollingHorizontally = function(scrollEvent) {
3697 if (!self.isScrollingVertically && !self.isScrollingHorizontally) {
3698 self.api.core.raise.scrollBegin(scrollEvent);
3699 }
3700 self.isScrollingHorizontally = true;
3701 if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) {
3702 debouncedHorizontalMinDelay(scrollEvent);
3703 }
3704 else {
3705 debouncedHorizontal(scrollEvent);
3706 }
3707 };
3708
3709 self.scrollbarHeight = 0;
3710 self.scrollbarWidth = 0;
3711 if (self.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3712 self.scrollbarHeight = gridUtil.getScrollbarWidth();
3713 }
3714
3715 if (self.options.enableVerticalScrollbar === uiGridConstants.scrollbars.ALWAYS) {
3716 self.scrollbarWidth = gridUtil.getScrollbarWidth();
3717 }
3718
3719
3720
3721 self.api = new GridApi(self);
3722
3723 /**
3724 * @ngdoc function
3725 * @name refresh
3726 * @methodOf ui.grid.core.api:PublicApi
3727 * @description Refresh the rendered grid on screen.
3728 * The refresh method re-runs both the columnProcessors and the
3729 * rowProcessors, as well as calling refreshCanvas to update all
3730 * the grid sizing. In general you should prefer to use queueGridRefresh
3731 * instead, which is basically a debounced version of refresh.
3732 *
3733 * If you only want to resize the grid, not regenerate all the rows
3734 * and columns, you should consider directly calling refreshCanvas instead.
3735 *
3736 */
3737 self.api.registerMethod( 'core', 'refresh', this.refresh );
3738
3739 /**
3740 * @ngdoc function
3741 * @name queueGridRefresh
3742 * @methodOf ui.grid.core.api:PublicApi
3743 * @description Request a refresh of the rendered grid on screen, if multiple
3744 * calls to queueGridRefresh are made within a digest cycle only one will execute.
3745 * The refresh method re-runs both the columnProcessors and the
3746 * rowProcessors, as well as calling refreshCanvas to update all
3747 * the grid sizing. In general you should prefer to use queueGridRefresh
3748 * instead, which is basically a debounced version of refresh.
3749 *
3750 */
3751 self.api.registerMethod( 'core', 'queueGridRefresh', this.queueGridRefresh );
3752
3753 /**
3754 * @ngdoc function
3755 * @name refreshRows
3756 * @methodOf ui.grid.core.api:PublicApi
3757 * @description Runs only the rowProcessors, columns remain as they were.
3758 * It then calls redrawInPlace and refreshCanvas, which adjust the grid sizing.
3759 * @returns {promise} promise that is resolved when render completes?
3760 *
3761 */
3762 self.api.registerMethod( 'core', 'refreshRows', this.refreshRows );
3763
3764 /**
3765 * @ngdoc function
3766 * @name queueRefresh
3767 * @methodOf ui.grid.core.api:PublicApi
3768 * @description Requests execution of refreshCanvas, if multiple requests are made
3769 * during a digest cycle only one will run. RefreshCanvas updates the grid sizing.
3770 * @returns {promise} promise that is resolved when render completes?
3771 *
3772 */
3773 self.api.registerMethod( 'core', 'queueRefresh', this.queueRefresh );
3774
3775 /**
3776 * @ngdoc function
3777 * @name handleWindowResize
3778 * @methodOf ui.grid.core.api:PublicApi
3779 * @description Trigger a grid resize, normally this would be picked
3780 * up by a watch on window size, but in some circumstances it is necessary
3781 * to call this manually
3782 * @returns {promise} promise that is resolved when render completes?
3783 *
3784 */
3785 self.api.registerMethod( 'core', 'handleWindowResize', this.handleWindowResize );
3786
3787
3788 /**
3789 * @ngdoc function
3790 * @name addRowHeaderColumn
3791 * @methodOf ui.grid.core.api:PublicApi
3792 * @description adds a row header column to the grid
3793 * @param {object} column def
3794 *
3795 */
3796 self.api.registerMethod( 'core', 'addRowHeaderColumn', this.addRowHeaderColumn );
3797
3798 /**
3799 * @ngdoc function
3800 * @name scrollToIfNecessary
3801 * @methodOf ui.grid.core.api:PublicApi
3802 * @description Scrolls the grid to make a certain row and column combo visible,
3803 * in the case that it is not completely visible on the screen already.
3804 * @param {GridRow} gridRow row to make visible
3805 * @param {GridCol} gridCol column to make visible
3806 * @returns {promise} a promise that is resolved when scrolling is complete
3807 *
3808 */
3809 self.api.registerMethod( 'core', 'scrollToIfNecessary', function(gridRow, gridCol) { return self.scrollToIfNecessary(gridRow, gridCol);} );
3810
3811 /**
3812 * @ngdoc function
3813 * @name scrollTo
3814 * @methodOf ui.grid.core.api:PublicApi
3815 * @description Scroll the grid such that the specified
3816 * row and column is in view
3817 * @param {object} rowEntity gridOptions.data[] array instance to make visible
3818 * @param {object} colDef to make visible
3819 * @returns {promise} a promise that is resolved after any scrolling is finished
3820 */
3821 self.api.registerMethod( 'core', 'scrollTo', function (rowEntity, colDef) { return self.scrollTo(rowEntity, colDef);} );
3822
3823 /**
3824 * @ngdoc function
3825 * @name registerRowsProcessor
3826 * @methodOf ui.grid.core.api:PublicApi
3827 * @description
3828 * Register a "rows processor" function. When the rows are updated,
3829 * the grid calls each registered "rows processor", which has a chance
3830 * to alter the set of rows (sorting, etc) as long as the count is not
3831 * modified.
3832 *
3833 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
3834 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3835 * return the updated rows list, which is passed to the next processor in the chain
3836 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3837 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
3838 *
3839 * At present allRowsVisible is running at 50, sort manipulations running at 60-65, filter is running at 100,
3840 * sort is at 200, grouping and treeview at 400-410, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
3841 */
3842 self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor );
3843
3844 /**
3845 * @ngdoc function
3846 * @name registerColumnsProcessor
3847 * @methodOf ui.grid.core.api:PublicApi
3848 * @description
3849 * Register a "columns processor" function. When the columns are updated,
3850 * the grid calls each registered "columns processor", which has a chance
3851 * to alter the set of columns as long as the count is not
3852 * modified.
3853 *
3854 * @param {function(renderedColumnsToProcess, rows )} processorFunction columns processor function, which
3855 * is run in the context of the grid (i.e. this for the function will be the grid), and must
3856 * return the updated columns list, which is passed to the next processor in the chain
3857 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
3858 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
3859 *
3860 * At present allRowsVisible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
3861 */
3862 self.api.registerMethod( 'core', 'registerColumnsProcessor', this.registerColumnsProcessor );
3863
3864
3865
3866 /**
3867 * @ngdoc function
3868 * @name sortHandleNulls
3869 * @methodOf ui.grid.core.api:PublicApi
3870 * @description A null handling method that can be used when building custom sort
3871 * functions
3872 * @example
3873 * <pre>
3874 * mySortFn = function(a, b) {
3875 * var nulls = $scope.gridApi.core.sortHandleNulls(a, b);
3876 * if ( nulls !== null ){
3877 * return nulls;
3878 * } else {
3879 * // your code for sorting here
3880 * };
3881 * </pre>
3882 * @param {object} a sort value a
3883 * @param {object} b sort value b
3884 * @returns {number} null if there were no nulls/undefineds, otherwise returns
3885 * a sort value that should be passed back from the sort function
3886 *
3887 */
3888 self.api.registerMethod( 'core', 'sortHandleNulls', rowSorter.handleNulls );
3889
3890
3891 /**
3892 * @ngdoc function
3893 * @name sortChanged
3894 * @methodOf ui.grid.core.api:PublicApi
3895 * @description The sort criteria on one or more columns has
3896 * changed. Provides as parameters the grid and the output of
3897 * getColumnSorting, which is an array of gridColumns
3898 * that have sorting on them, sorted in priority order.
3899 *
3900 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3901 * @param {Function} callBack Will be called when the event is emited. The function passes back an array of columns with
3902 * sorts on them, in priority order.
3903 *
3904 * @example
3905 * <pre>
3906 * gridApi.core.on.sortChanged( $scope, function(sortColumns){
3907 * // do something
3908 * });
3909 * </pre>
3910 */
3911 self.api.registerEvent( 'core', 'sortChanged' );
3912
3913 /**
3914 * @ngdoc function
3915 * @name columnVisibilityChanged
3916 * @methodOf ui.grid.core.api:PublicApi
3917 * @description The visibility of a column has changed,
3918 * the column itself is passed out as a parameter of the event.
3919 *
3920 * @param {$scope} scope The scope of the controller. This is used to deregister this event when the scope is destroyed.
3921 * @param {Function} callBack Will be called when the event is emited. The function passes back the GridCol that has changed.
3922 *
3923 * @example
3924 * <pre>
3925 * gridApi.core.on.columnVisibilityChanged( $scope, function (column) {
3926 * // do something
3927 * } );
3928 * </pre>
3929 */
3930 self.api.registerEvent( 'core', 'columnVisibilityChanged' );
3931
3932 /**
3933 * @ngdoc method
3934 * @name notifyDataChange
3935 * @methodOf ui.grid.core.api:PublicApi
3936 * @description Notify the grid that a data or config change has occurred,
3937 * where that change isn't something the grid was otherwise noticing. This
3938 * might be particularly relevant where you've changed values within the data
3939 * and you'd like cell classes to be re-evaluated, or changed config within
3940 * the columnDef and you'd like headerCellClasses to be re-evaluated.
3941 * @param {string} type one of the
3942 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN), which tells
3943 * us which refreshes to fire.
3944 *
3945 */
3946 self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange );
3947
3948 /**
3949 * @ngdoc method
3950 * @name clearAllFilters
3951 * @methodOf ui.grid.core.api:PublicApi
3952 * @description Clears all filters and optionally refreshes the visible rows.
3953 * @param {object} refreshRows Defaults to true.
3954 * @param {object} clearConditions Defaults to false.
3955 * @param {object} clearFlags Defaults to false.
3956 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
3957 */
3958 self.api.registerMethod('core', 'clearAllFilters', this.clearAllFilters);
3959
3960 self.registerDataChangeCallback( self.columnRefreshCallback, [uiGridConstants.dataChange.COLUMN]);
3961 self.registerDataChangeCallback( self.processRowsCallback, [uiGridConstants.dataChange.EDIT]);
3962 self.registerDataChangeCallback( self.updateFooterHeightCallback, [uiGridConstants.dataChange.OPTIONS]);
3963
3964 self.registerStyleComputation({
3965 priority: 10,
3966 func: self.getFooterStyles
3967 });
3968 };
3969
3970 Grid.prototype.calcFooterHeight = function () {
3971 if (!this.hasFooter()) {
3972 return 0;
3973 }
3974
3975 var height = 0;
3976 if (this.options.showGridFooter) {
3977 height += this.options.gridFooterHeight;
3978 }
3979
3980 height += this.calcColumnFooterHeight();
3981
3982 return height;
3983 };
3984
3985 Grid.prototype.calcColumnFooterHeight = function () {
3986 var height = 0;
3987
3988 if (this.options.showColumnFooter) {
3989 height += this.options.columnFooterHeight;
3990 }
3991
3992 return height;
3993 };
3994
3995 Grid.prototype.getFooterStyles = function () {
3996 var style = '.grid' + this.id + ' .ui-grid-footer-aggregates-row { height: ' + this.options.columnFooterHeight + 'px; }';
3997 style += ' .grid' + this.id + ' .ui-grid-footer-info { height: ' + this.options.gridFooterHeight + 'px; }';
3998 return style;
3999 };
4000
4001 Grid.prototype.hasFooter = function () {
4002 return this.options.showGridFooter || this.options.showColumnFooter;
4003 };
4004
4005 /**
4006 * @ngdoc function
4007 * @name isRTL
4008 * @methodOf ui.grid.class:Grid
4009 * @description Returns true if grid is RightToLeft
4010 */
4011 Grid.prototype.isRTL = function () {
4012 return this.rtl;
4013 };
4014
4015
4016 /**
4017 * @ngdoc function
4018 * @name registerColumnBuilder
4019 * @methodOf ui.grid.class:Grid
4020 * @description When the build creates columns from column definitions, the columnbuilders will be called to add
4021 * additional properties to the column.
4022 * @param {function(colDef, col, gridOptions)} columnBuilder function to be called
4023 */
4024 Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnBuilder) {
4025 this.columnBuilders.push(columnBuilder);
4026 };
4027
4028 /**
4029 * @ngdoc function
4030 * @name buildColumnDefsFromData
4031 * @methodOf ui.grid.class:Grid
4032 * @description Populates columnDefs from the provided data
4033 * @param {function(colDef, col, gridOptions)} rowBuilder function to be called
4034 */
4035 Grid.prototype.buildColumnDefsFromData = function (dataRows){
4036 this.options.columnDefs = gridUtil.getColumnsFromData(dataRows, this.options.excludeProperties);
4037 };
4038
4039 /**
4040 * @ngdoc function
4041 * @name registerRowBuilder
4042 * @methodOf ui.grid.class:Grid
4043 * @description When the build creates rows from gridOptions.data, the rowBuilders will be called to add
4044 * additional properties to the row.
4045 * @param {function(row, gridOptions)} rowBuilder function to be called
4046 */
4047 Grid.prototype.registerRowBuilder = function registerRowBuilder(rowBuilder) {
4048 this.rowBuilders.push(rowBuilder);
4049 };
4050
4051
4052 /**
4053 * @ngdoc function
4054 * @name registerDataChangeCallback
4055 * @methodOf ui.grid.class:Grid
4056 * @description When a data change occurs, the data change callbacks of the specified type
4057 * will be called. The rules are:
4058 *
4059 * - when the data watch fires, that is considered a ROW change (the data watch only notices
4060 * added or removed rows)
4061 * - when the api is called to inform us of a change, the declared type of that change is used
4062 * - when a cell edit completes, the EDIT callbacks are triggered
4063 * - when the columnDef watch fires, the COLUMN callbacks are triggered
4064 * - when the options watch fires, the OPTIONS callbacks are triggered
4065 *
4066 * For a given event:
4067 * - ALL calls ROW, EDIT, COLUMN, OPTIONS and ALL callbacks
4068 * - ROW calls ROW and ALL callbacks
4069 * - EDIT calls EDIT and ALL callbacks
4070 * - COLUMN calls COLUMN and ALL callbacks
4071 * - OPTIONS calls OPTIONS and ALL callbacks
4072 *
4073 * @param {function(grid)} callback function to be called
4074 * @param {array} types the types of data change you want to be informed of. Values from
4075 * the uiGridConstants.dataChange values ( ALL, EDIT, ROW, COLUMN, OPTIONS ). Optional and defaults to
4076 * ALL
4077 * @returns {function} deregister function - a function that can be called to deregister this callback
4078 */
4079 Grid.prototype.registerDataChangeCallback = function registerDataChangeCallback(callback, types, _this) {
4080 var uid = gridUtil.nextUid();
4081 if ( !types ){
4082 types = [uiGridConstants.dataChange.ALL];
4083 }
4084 if ( !Array.isArray(types)){
4085 gridUtil.logError("Expected types to be an array or null in registerDataChangeCallback, value passed was: " + types );
4086 }
4087 this.dataChangeCallbacks[uid] = { callback: callback, types: types, _this:_this };
4088
4089 var self = this;
4090 var deregisterFunction = function() {
4091 delete self.dataChangeCallbacks[uid];
4092 };
4093 return deregisterFunction;
4094 };
4095
4096 /**
4097 * @ngdoc function
4098 * @name callDataChangeCallbacks
4099 * @methodOf ui.grid.class:Grid
4100 * @description Calls the callbacks based on the type of data change that
4101 * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the
4102 * event type is matching, or if the type is ALL.
4103 * @param {number} type the type of event that occurred - one of the
4104 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN, OPTIONS)
4105 */
4106 Grid.prototype.callDataChangeCallbacks = function callDataChangeCallbacks(type, options) {
4107 angular.forEach( this.dataChangeCallbacks, function( callback, uid ){
4108 if ( callback.types.indexOf( uiGridConstants.dataChange.ALL ) !== -1 ||
4109 callback.types.indexOf( type ) !== -1 ||
4110 type === uiGridConstants.dataChange.ALL ) {
4111 if (callback._this) {
4112 callback.callback.apply(callback._this,this);
4113 }
4114 else {
4115 callback.callback( this );
4116 }
4117 }
4118 }, this);
4119 };
4120
4121 /**
4122 * @ngdoc function
4123 * @name notifyDataChange
4124 * @methodOf ui.grid.class:Grid
4125 * @description Notifies us that a data change has occurred, used in the public
4126 * api for users to tell us when they've changed data or some other event that
4127 * our watches cannot pick up
4128 * @param {string} type the type of event that occurred - one of the
4129 * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN)
4130 */
4131 Grid.prototype.notifyDataChange = function notifyDataChange(type) {
4132 var constants = uiGridConstants.dataChange;
4133 if ( type === constants.ALL ||
4134 type === constants.COLUMN ||
4135 type === constants.EDIT ||
4136 type === constants.ROW ||
4137 type === constants.OPTIONS ){
4138 this.callDataChangeCallbacks( type );
4139 } else {
4140 gridUtil.logError("Notified of a data change, but the type was not recognised, so no action taken, type was: " + type);
4141 }
4142 };
4143
4144
4145 /**
4146 * @ngdoc function
4147 * @name columnRefreshCallback
4148 * @methodOf ui.grid.class:Grid
4149 * @description refreshes the grid when a column refresh
4150 * is notified, which triggers handling of the visible flag.
4151 * This is called on uiGridConstants.dataChange.COLUMN, and is
4152 * registered as a dataChangeCallback in grid.js
4153 * @param {string} name column name
4154 */
4155 Grid.prototype.columnRefreshCallback = function columnRefreshCallback( grid ){
4156 grid.buildColumns();
4157 grid.queueGridRefresh();
4158 };
4159
4160
4161 /**
4162 * @ngdoc function
4163 * @name processRowsCallback
4164 * @methodOf ui.grid.class:Grid
4165 * @description calls the row processors, specifically
4166 * intended to reset the sorting when an edit is called,
4167 * registered as a dataChangeCallback on uiGridConstants.dataChange.EDIT
4168 * @param {string} name column name
4169 */
4170 Grid.prototype.processRowsCallback = function processRowsCallback( grid ){
4171 grid.queueGridRefresh();
4172 };
4173
4174
4175 /**
4176 * @ngdoc function
4177 * @name updateFooterHeightCallback
4178 * @methodOf ui.grid.class:Grid
4179 * @description recalculates the footer height,
4180 * registered as a dataChangeCallback on uiGridConstants.dataChange.OPTIONS
4181 * @param {string} name column name
4182 */
4183 Grid.prototype.updateFooterHeightCallback = function updateFooterHeightCallback( grid ){
4184 grid.footerHeight = grid.calcFooterHeight();
4185 grid.columnFooterHeight = grid.calcColumnFooterHeight();
4186 };
4187
4188
4189 /**
4190 * @ngdoc function
4191 * @name getColumn
4192 * @methodOf ui.grid.class:Grid
4193 * @description returns a grid column for the column name
4194 * @param {string} name column name
4195 */
4196 Grid.prototype.getColumn = function getColumn(name) {
4197 var columns = this.columns.filter(function (column) {
4198 return column.colDef.name === name;
4199 });
4200 return columns.length > 0 ? columns[0] : null;
4201 };
4202
4203 /**
4204 * @ngdoc function
4205 * @name getColDef
4206 * @methodOf ui.grid.class:Grid
4207 * @description returns a grid colDef for the column name
4208 * @param {string} name column.field
4209 */
4210 Grid.prototype.getColDef = function getColDef(name) {
4211 var colDefs = this.options.columnDefs.filter(function (colDef) {
4212 return colDef.name === name;
4213 });
4214 return colDefs.length > 0 ? colDefs[0] : null;
4215 };
4216
4217 /**
4218 * @ngdoc function
4219 * @name assignTypes
4220 * @methodOf ui.grid.class:Grid
4221 * @description uses the first row of data to assign colDef.type for any types not defined.
4222 */
4223 /**
4224 * @ngdoc property
4225 * @name type
4226 * @propertyOf ui.grid.class:GridOptions.columnDef
4227 * @description the type of the column, used in sorting. If not provided then the
4228 * grid will guess the type. Add this only if the grid guessing is not to your
4229 * satisfaction. One of:
4230 * - 'string'
4231 * - 'boolean'
4232 * - 'number'
4233 * - 'date'
4234 * - 'object'
4235 * - 'numberStr'
4236 * Note that if you choose date, your dates should be in a javascript date type
4237 *
4238 */
4239 Grid.prototype.assignTypes = function(){
4240 var self = this;
4241 self.options.columnDefs.forEach(function (colDef, index) {
4242
4243 //Assign colDef type if not specified
4244 if (!colDef.type) {
4245 var col = new GridColumn(colDef, index, self);
4246 var firstRow = self.rows.length > 0 ? self.rows[0] : null;
4247 if (firstRow) {
4248 colDef.type = gridUtil.guessType(self.getCellValue(firstRow, col));
4249 }
4250 else {
4251 colDef.type = 'string';
4252 }
4253 }
4254 });
4255 };
4256
4257
4258 /**
4259 * @ngdoc function
4260 * @name isRowHeaderColumn
4261 * @methodOf ui.grid.class:Grid
4262 * @description returns true if the column is a row Header
4263 * @param {object} column column
4264 */
4265 Grid.prototype.isRowHeaderColumn = function isRowHeaderColumn(column) {
4266 return this.rowHeaderColumns.indexOf(column) !== -1;
4267 };
4268
4269 /**
4270 * @ngdoc function
4271 * @name addRowHeaderColumn
4272 * @methodOf ui.grid.class:Grid
4273 * @description adds a row header column to the grid
4274 * @param {object} column def
4275 */
4276 Grid.prototype.addRowHeaderColumn = function addRowHeaderColumn(colDef) {
4277 var self = this;
4278 var rowHeaderCol = new GridColumn(colDef, gridUtil.nextUid(), self);
4279 rowHeaderCol.isRowHeader = true;
4280 if (self.isRTL()) {
4281 self.createRightContainer();
4282 rowHeaderCol.renderContainer = 'right';
4283 }
4284 else {
4285 self.createLeftContainer();
4286 rowHeaderCol.renderContainer = 'left';
4287 }
4288
4289 // relies on the default column builder being first in array, as it is instantiated
4290 // as part of grid creation
4291 self.columnBuilders[0](colDef,rowHeaderCol,self.options)
4292 .then(function(){
4293 rowHeaderCol.enableFiltering = false;
4294 rowHeaderCol.enableSorting = false;
4295 rowHeaderCol.enableHiding = false;
4296 self.rowHeaderColumns.push(rowHeaderCol);
4297 self.buildColumns()
4298 .then( function() {
4299 self.preCompileCellTemplates();
4300 self.queueGridRefresh();
4301 });
4302 });
4303 };
4304
4305 /**
4306 * @ngdoc function
4307 * @name getOnlyDataColumns
4308 * @methodOf ui.grid.class:Grid
4309 * @description returns all columns except for rowHeader columns
4310 */
4311 Grid.prototype.getOnlyDataColumns = function getOnlyDataColumns() {
4312 var self = this;
4313 var cols = [];
4314 self.columns.forEach(function (col) {
4315 if (self.rowHeaderColumns.indexOf(col) === -1) {
4316 cols.push(col);
4317 }
4318 });
4319 return cols;
4320 };
4321
4322 /**
4323 * @ngdoc function
4324 * @name buildColumns
4325 * @methodOf ui.grid.class:Grid
4326 * @description creates GridColumn objects from the columnDefinition. Calls each registered
4327 * columnBuilder to further process the column
4328 * @param {object} options An object contains options to use when building columns
4329 *
4330 * * **orderByColumnDefs**: defaults to **false**. When true, `buildColumns` will reorder existing columns according to the order within the column definitions.
4331 *
4332 * @returns {Promise} a promise to load any needed column resources
4333 */
4334 Grid.prototype.buildColumns = function buildColumns(opts) {
4335 var options = {
4336 orderByColumnDefs: false
4337 };
4338
4339 angular.extend(options, opts);
4340
4341 // gridUtil.logDebug('buildColumns');
4342 var self = this;
4343 var builderPromises = [];
4344 var headerOffset = self.rowHeaderColumns.length;
4345 var i;
4346
4347 // Remove any columns for which a columnDef cannot be found
4348 // Deliberately don't use forEach, as it doesn't like splice being called in the middle
4349 // Also don't cache columns.length, as it will change during this operation
4350 for (i = 0; i < self.columns.length; i++){
4351 if (!self.getColDef(self.columns[i].name)) {
4352 self.columns.splice(i, 1);
4353 i--;
4354 }
4355 }
4356
4357 //add row header columns to the grid columns array _after_ columns without columnDefs have been removed
4358 self.rowHeaderColumns.forEach(function (rowHeaderColumn) {
4359 self.columns.unshift(rowHeaderColumn);
4360 });
4361
4362
4363 // look at each column def, and update column properties to match. If the column def
4364 // doesn't have a column, then splice in a new gridCol
4365 self.options.columnDefs.forEach(function (colDef, index) {
4366 self.preprocessColDef(colDef);
4367 var col = self.getColumn(colDef.name);
4368
4369 if (!col) {
4370 col = new GridColumn(colDef, gridUtil.nextUid(), self);
4371 self.columns.splice(index + headerOffset, 0, col);
4372 }
4373 else {
4374 // tell updateColumnDef that the column was pre-existing
4375 col.updateColumnDef(colDef, false);
4376 }
4377
4378 self.columnBuilders.forEach(function (builder) {
4379 builderPromises.push(builder.call(self, colDef, col, self.options));
4380 });
4381 });
4382
4383 /*** Reorder columns if necessary ***/
4384 if (!!options.orderByColumnDefs) {
4385 // Create a shallow copy of the columns as a cache
4386 var columnCache = self.columns.slice(0);
4387
4388 // We need to allow for the "row headers" when mapping from the column defs array to the columns array
4389 // If we have a row header in columns[0] and don't account for it we'll overwrite it with the column in columnDefs[0]
4390
4391 // Go through all the column defs, use the shorter of columns length and colDefs.length because if a user has given two columns the same name then
4392 // columns will be shorter than columnDefs. In this situation we'll avoid an error, but the user will still get an unexpected result
4393 var len = Math.min(self.options.columnDefs.length, self.columns.length);
4394 for (i = 0; i < len; i++) {
4395 // If the column at this index has a different name than the column at the same index in the column defs...
4396 if (self.columns[i + headerOffset].name !== self.options.columnDefs[i].name) {
4397 // Replace the one in the cache with the appropriate column
4398 columnCache[i + headerOffset] = self.getColumn(self.options.columnDefs[i].name);
4399 }
4400 else {
4401 // Otherwise just copy over the one from the initial columns
4402 columnCache[i + headerOffset] = self.columns[i + headerOffset];
4403 }
4404 }
4405
4406 // Empty out the columns array, non-destructively
4407 self.columns.length = 0;
4408
4409 // And splice in the updated, ordered columns from the cache
4410 Array.prototype.splice.apply(self.columns, [0, 0].concat(columnCache));
4411 }
4412
4413 return $q.all(builderPromises).then(function(){
4414 if (self.rows.length > 0){
4415 self.assignTypes();
4416 }
4417 });
4418 };
4419
4420 /**
4421 * @ngdoc function
4422 * @name preCompileCellTemplates
4423 * @methodOf ui.grid.class:Grid
4424 * @description precompiles all cell templates
4425 */
4426 Grid.prototype.preCompileCellTemplates = function() {
4427 var self = this;
4428
4429 var preCompileTemplate = function( col ) {
4430 var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col));
4431 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
4432
4433 var compiledElementFn = $compile(html);
4434 col.compiledElementFn = compiledElementFn;
4435
4436 if (col.compiledElementFnDefer) {
4437 col.compiledElementFnDefer.resolve(col.compiledElementFn);
4438 }
4439 };
4440
4441 this.columns.forEach(function (col) {
4442 if ( col.cellTemplate ){
4443 preCompileTemplate( col );
4444 } else if ( col.cellTemplatePromise ){
4445 col.cellTemplatePromise.then( function() {
4446 preCompileTemplate( col );
4447 });
4448 }
4449 });
4450 };
4451
4452 /**
4453 * @ngdoc function
4454 * @name getGridQualifiedColField
4455 * @methodOf ui.grid.class:Grid
4456 * @description Returns the $parse-able accessor for a column within its $scope
4457 * @param {GridColumn} col col object
4458 */
4459 Grid.prototype.getQualifiedColField = function (col) {
4460 return 'row.entity.' + gridUtil.preEval(col.field);
4461 };
4462
4463 /**
4464 * @ngdoc function
4465 * @name createLeftContainer
4466 * @methodOf ui.grid.class:Grid
4467 * @description creates the left render container if it doesn't already exist
4468 */
4469 Grid.prototype.createLeftContainer = function() {
4470 if (!this.hasLeftContainer()) {
4471 this.renderContainers.left = new GridRenderContainer('left', this, { disableColumnOffset: true });
4472 }
4473 };
4474
4475 /**
4476 * @ngdoc function
4477 * @name createRightContainer
4478 * @methodOf ui.grid.class:Grid
4479 * @description creates the right render container if it doesn't already exist
4480 */
4481 Grid.prototype.createRightContainer = function() {
4482 if (!this.hasRightContainer()) {
4483 this.renderContainers.right = new GridRenderContainer('right', this, { disableColumnOffset: true });
4484 }
4485 };
4486
4487 /**
4488 * @ngdoc function
4489 * @name hasLeftContainer
4490 * @methodOf ui.grid.class:Grid
4491 * @description returns true if leftContainer exists
4492 */
4493 Grid.prototype.hasLeftContainer = function() {
4494 return this.renderContainers.left !== undefined;
4495 };
4496
4497 /**
4498 * @ngdoc function
4499 * @name hasRightContainer
4500 * @methodOf ui.grid.class:Grid
4501 * @description returns true if rightContainer exists
4502 */
4503 Grid.prototype.hasRightContainer = function() {
4504 return this.renderContainers.right !== undefined;
4505 };
4506
4507
4508 /**
4509 * undocumented function
4510 * @name preprocessColDef
4511 * @methodOf ui.grid.class:Grid
4512 * @description defaults the name property from field to maintain backwards compatibility with 2.x
4513 * validates that name or field is present
4514 */
4515 Grid.prototype.preprocessColDef = function preprocessColDef(colDef) {
4516 var self = this;
4517
4518 if (!colDef.field && !colDef.name) {
4519 throw new Error('colDef.name or colDef.field property is required');
4520 }
4521
4522 //maintain backwards compatibility with 2.x
4523 //field was required in 2.x. now name is required
4524 if (colDef.name === undefined && colDef.field !== undefined) {
4525 // See if the column name already exists:
4526 var newName = colDef.field,
4527 counter = 2;
4528 while (self.getColumn(newName)) {
4529 newName = colDef.field + counter.toString();
4530 counter++;
4531 }
4532 colDef.name = newName;
4533 }
4534 };
4535
4536 // Return a list of items that exist in the `n` array but not the `o` array. Uses optional property accessors passed as third & fourth parameters
4537 Grid.prototype.newInN = function newInN(o, n, oAccessor, nAccessor) {
4538 var self = this;
4539
4540 var t = [];
4541 for (var i = 0; i < n.length; i++) {
4542 var nV = nAccessor ? n[i][nAccessor] : n[i];
4543
4544 var found = false;
4545 for (var j = 0; j < o.length; j++) {
4546 var oV = oAccessor ? o[j][oAccessor] : o[j];
4547 if (self.options.rowEquality(nV, oV)) {
4548 found = true;
4549 break;
4550 }
4551 }
4552 if (!found) {
4553 t.push(nV);
4554 }
4555 }
4556
4557 return t;
4558 };
4559
4560 /**
4561 * @ngdoc function
4562 * @name getRow
4563 * @methodOf ui.grid.class:Grid
4564 * @description returns the GridRow that contains the rowEntity
4565 * @param {object} rowEntity the gridOptions.data array element instance
4566 * @param {array} rows [optional] the rows to look in - if not provided then
4567 * looks in grid.rows
4568 */
4569 Grid.prototype.getRow = function getRow(rowEntity, lookInRows) {
4570 var self = this;
4571
4572 lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows;
4573
4574 var rows = lookInRows.filter(function (row) {
4575 return self.options.rowEquality(row.entity, rowEntity);
4576 });
4577 return rows.length > 0 ? rows[0] : null;
4578 };
4579
4580
4581 /**
4582 * @ngdoc function
4583 * @name modifyRows
4584 * @methodOf ui.grid.class:Grid
4585 * @description creates or removes GridRow objects from the newRawData array. Calls each registered
4586 * rowBuilder to further process the row
4587 * @param {array} newRawData Modified set of data
4588 *
4589 * This method aims to achieve three things:
4590 * 1. the resulting rows array is in the same order as the newRawData, we'll call
4591 * rowsProcessors immediately after to sort the data anyway
4592 * 2. if we have row hashing available, we try to use the rowHash to find the row
4593 * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected
4594 *
4595 * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates
4596 * the newRows and newHash
4597 *
4598 * ```
4599 * newRawData.forEach newEntity
4600 * if (hashing enabled)
4601 * check oldHash for newEntity
4602 * else
4603 * look for old row directly in oldRows
4604 * if !oldRowFound // must be a new row
4605 * create newRow
4606 * append to the newRows and add to newHash
4607 * run the processors
4608 * ```
4609 *
4610 * Rows are identified using the hashKey if configured. If not configured, then rows
4611 * are identified using the gridOptions.rowEquality function
4612 *
4613 * This method is useful when trying to select rows immediately after loading data without
4614 * using a $timeout/$interval, e.g.:
4615 *
4616 * $scope.gridOptions.data = someData;
4617 * $scope.gridApi.grid.modifyRows($scope.gridOptions.data);
4618 * $scope.gridApi.selection.selectRow($scope.gridOptions.data[0]);
4619 *
4620 * OR to persist row selection after data update (e.g. rows selected, new data loaded, want
4621 * originally selected rows to be re-selected))
4622 */
4623 Grid.prototype.modifyRows = function modifyRows(newRawData) {
4624 var self = this;
4625 var oldRows = self.rows.slice(0);
4626 var oldRowHash = self.rowHashMap || self.createRowHashMap();
4627 self.rowHashMap = self.createRowHashMap();
4628 self.rows.length = 0;
4629
4630 newRawData.forEach( function( newEntity, i ) {
4631 var newRow;
4632 if ( self.options.enableRowHashing ){
4633 // if hashing is enabled, then this row will be in the hash if we already know about it
4634 newRow = oldRowHash.get( newEntity );
4635 } else {
4636 // otherwise, manually search the oldRows to see if we can find this row
4637 newRow = self.getRow(newEntity, oldRows);
4638 }
4639
4640 // if we didn't find the row, it must be new, so create it
4641 if ( !newRow ){
4642 newRow = self.processRowBuilders(new GridRow(newEntity, i, self));
4643 }
4644
4645 self.rows.push( newRow );
4646 self.rowHashMap.put( newEntity, newRow );
4647 });
4648
4649 self.assignTypes();
4650
4651 var p1 = $q.when(self.processRowsProcessors(self.rows))
4652 .then(function (renderableRows) {
4653 return self.setVisibleRows(renderableRows);
4654 });
4655
4656 var p2 = $q.when(self.processColumnsProcessors(self.columns))
4657 .then(function (renderableColumns) {
4658 return self.setVisibleColumns(renderableColumns);
4659 });
4660
4661 return $q.all([p1, p2]);
4662 };
4663
4664
4665 /**
4666 * Private Undocumented Method
4667 * @name addRows
4668 * @methodOf ui.grid.class:Grid
4669 * @description adds the newRawData array of rows to the grid and calls all registered
4670 * rowBuilders. this keyword will reference the grid
4671 */
4672 Grid.prototype.addRows = function addRows(newRawData) {
4673 var self = this;
4674
4675 var existingRowCount = self.rows.length;
4676 for (var i = 0; i < newRawData.length; i++) {
4677 var newRow = self.processRowBuilders(new GridRow(newRawData[i], i + existingRowCount, self));
4678
4679 if (self.options.enableRowHashing) {
4680 var found = self.rowHashMap.get(newRow.entity);
4681 if (found) {
4682 found.row = newRow;
4683 }
4684 }
4685
4686 self.rows.push(newRow);
4687 }
4688 };
4689
4690 /**
4691 * @ngdoc function
4692 * @name processRowBuilders
4693 * @methodOf ui.grid.class:Grid
4694 * @description processes all RowBuilders for the gridRow
4695 * @param {GridRow} gridRow reference to gridRow
4696 * @returns {GridRow} the gridRow with all additional behavior added
4697 */
4698 Grid.prototype.processRowBuilders = function processRowBuilders(gridRow) {
4699 var self = this;
4700
4701 self.rowBuilders.forEach(function (builder) {
4702 builder.call(self, gridRow, self.options);
4703 });
4704
4705 return gridRow;
4706 };
4707
4708 /**
4709 * @ngdoc function
4710 * @name registerStyleComputation
4711 * @methodOf ui.grid.class:Grid
4712 * @description registered a styleComputation function
4713 *
4714 * If the function returns a value it will be appended into the grid's `<style>` block
4715 * @param {function($scope)} styleComputation function
4716 */
4717 Grid.prototype.registerStyleComputation = function registerStyleComputation(styleComputationInfo) {
4718 this.styleComputations.push(styleComputationInfo);
4719 };
4720
4721
4722 // NOTE (c0bra): We already have rowBuilders. I think these do exactly the same thing...
4723 // Grid.prototype.registerRowFilter = function(filter) {
4724 // // TODO(c0bra): validate filter?
4725
4726 // this.rowFilters.push(filter);
4727 // };
4728
4729 // Grid.prototype.removeRowFilter = function(filter) {
4730 // var idx = this.rowFilters.indexOf(filter);
4731
4732 // if (typeof(idx) !== 'undefined' && idx !== undefined) {
4733 // this.rowFilters.slice(idx, 1);
4734 // }
4735 // };
4736
4737 // Grid.prototype.processRowFilters = function(rows) {
4738 // var self = this;
4739 // self.rowFilters.forEach(function (filter) {
4740 // filter.call(self, rows);
4741 // });
4742 // };
4743
4744
4745 /**
4746 * @ngdoc function
4747 * @name registerRowsProcessor
4748 * @methodOf ui.grid.class:Grid
4749 * @description
4750 *
4751 * Register a "rows processor" function. When the rows are updated,
4752 * the grid calls each registered "rows processor", which has a chance
4753 * to alter the set of rows (sorting, etc) as long as the count is not
4754 * modified.
4755 *
4756 * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which
4757 * is run in the context of the grid (i.e. this for the function will be the grid), and must
4758 * return the updated rows list, which is passed to the next processor in the chain
4759 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4760 * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier.
4761 *
4762 * At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
4763 *
4764 */
4765 Grid.prototype.registerRowsProcessor = function registerRowsProcessor(processor, priority) {
4766 if (!angular.isFunction(processor)) {
4767 throw 'Attempt to register non-function rows processor: ' + processor;
4768 }
4769
4770 this.rowsProcessors.push({processor: processor, priority: priority});
4771 this.rowsProcessors.sort(function sortByPriority( a, b ){
4772 return a.priority - b.priority;
4773 });
4774 };
4775
4776 /**
4777 * @ngdoc function
4778 * @name removeRowsProcessor
4779 * @methodOf ui.grid.class:Grid
4780 * @param {function(renderableRows)} rows processor function
4781 * @description Remove a registered rows processor
4782 */
4783 Grid.prototype.removeRowsProcessor = function removeRowsProcessor(processor) {
4784 var idx = -1;
4785 this.rowsProcessors.forEach(function(rowsProcessor, index){
4786 if ( rowsProcessor.processor === processor ){
4787 idx = index;
4788 }
4789 });
4790
4791 if ( idx !== -1 ) {
4792 this.rowsProcessors.splice(idx, 1);
4793 }
4794 };
4795
4796 /**
4797 * Private Undocumented Method
4798 * @name processRowsProcessors
4799 * @methodOf ui.grid.class:Grid
4800 * @param {Array[GridRow]} The array of "renderable" rows
4801 * @param {Array[GridColumn]} The array of columns
4802 * @description Run all the registered rows processors on the array of renderable rows
4803 */
4804 Grid.prototype.processRowsProcessors = function processRowsProcessors(renderableRows) {
4805 var self = this;
4806
4807 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4808 var myRenderableRows = renderableRows.slice(0);
4809
4810 // Return myRenderableRows with no processing if we have no rows processors
4811 if (self.rowsProcessors.length === 0) {
4812 return $q.when(myRenderableRows);
4813 }
4814
4815 // Counter for iterating through rows processors
4816 var i = 0;
4817
4818 // Promise for when we're done with all the processors
4819 var finished = $q.defer();
4820
4821 // This function will call the processor in self.rowsProcessors at index 'i', and then
4822 // when done will call the next processor in the list, using the output from the processor
4823 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4824 //
4825 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4826 // the result.
4827 function startProcessor(i, renderedRowsToProcess) {
4828 // Get the processor at 'i'
4829 var processor = self.rowsProcessors[i].processor;
4830
4831 // Call the processor, passing in the rows to process and the current columns
4832 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
4833 return $q.when( processor.call(self, renderedRowsToProcess, self.columns) )
4834 .then(function handleProcessedRows(processedRows) {
4835 // Check for errors
4836 if (!processedRows) {
4837 throw "Processor at index " + i + " did not return a set of renderable rows";
4838 }
4839
4840 if (!angular.isArray(processedRows)) {
4841 throw "Processor at index " + i + " did not return an array";
4842 }
4843
4844 // Processor is done, increment the counter
4845 i++;
4846
4847 // If we're not done with the processors, call the next one
4848 if (i <= self.rowsProcessors.length - 1) {
4849 return startProcessor(i, processedRows);
4850 }
4851 // We're done! Resolve the 'finished' promise
4852 else {
4853 finished.resolve(processedRows);
4854 }
4855 });
4856 }
4857
4858 // Start on the first processor
4859 startProcessor(0, myRenderableRows);
4860
4861 return finished.promise;
4862 };
4863
4864 Grid.prototype.setVisibleRows = function setVisibleRows(rows) {
4865 var self = this;
4866
4867 // Reset all the render container row caches
4868 for (var i in self.renderContainers) {
4869 var container = self.renderContainers[i];
4870
4871 container.canvasHeightShouldUpdate = true;
4872
4873 if ( typeof(container.visibleRowCache) === 'undefined' ){
4874 container.visibleRowCache = [];
4875 } else {
4876 container.visibleRowCache.length = 0;
4877 }
4878 }
4879
4880 // rows.forEach(function (row) {
4881 for (var ri = 0; ri < rows.length; ri++) {
4882 var row = rows[ri];
4883
4884 var targetContainer = (typeof(row.renderContainer) !== 'undefined' && row.renderContainer) ? row.renderContainer : 'body';
4885
4886 // If the row is visible
4887 if (row.visible) {
4888 self.renderContainers[targetContainer].visibleRowCache.push(row);
4889 }
4890 }
4891 self.api.core.raise.rowsRendered(this.api);
4892 };
4893
4894 /**
4895 * @ngdoc function
4896 * @name registerColumnsProcessor
4897 * @methodOf ui.grid.class:Grid
4898 * @param {function(renderedColumnsToProcess, rows)} columnProcessor column processor function, which
4899 * is run in the context of the grid (i.e. this for the function will be the grid), and
4900 * which must return an updated renderedColumnsToProcess which can be passed to the next processor
4901 * in the chain
4902 * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room
4903 * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier.
4904 *
4905 * At present all rows visible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last)
4906 * @description
4907
4908 Register a "columns processor" function. When the columns are updated,
4909 the grid calls each registered "columns processor", which has a chance
4910 to alter the set of columns, as long as the count is not modified.
4911 */
4912 Grid.prototype.registerColumnsProcessor = function registerColumnsProcessor(processor, priority) {
4913 if (!angular.isFunction(processor)) {
4914 throw 'Attempt to register non-function rows processor: ' + processor;
4915 }
4916
4917 this.columnsProcessors.push({processor: processor, priority: priority});
4918 this.columnsProcessors.sort(function sortByPriority( a, b ){
4919 return a.priority - b.priority;
4920 });
4921 };
4922
4923 Grid.prototype.removeColumnsProcessor = function removeColumnsProcessor(processor) {
4924 var idx = this.columnsProcessors.indexOf(processor);
4925
4926 if (typeof(idx) !== 'undefined' && idx !== undefined) {
4927 this.columnsProcessors.splice(idx, 1);
4928 }
4929 };
4930
4931 Grid.prototype.processColumnsProcessors = function processColumnsProcessors(renderableColumns) {
4932 var self = this;
4933
4934 // Create a shallow copy of the rows so that we can safely sort them without altering the original grid.rows sort order
4935 var myRenderableColumns = renderableColumns.slice(0);
4936
4937 // Return myRenderableRows with no processing if we have no rows processors
4938 if (self.columnsProcessors.length === 0) {
4939 return $q.when(myRenderableColumns);
4940 }
4941
4942 // Counter for iterating through rows processors
4943 var i = 0;
4944
4945 // Promise for when we're done with all the processors
4946 var finished = $q.defer();
4947
4948 // This function will call the processor in self.rowsProcessors at index 'i', and then
4949 // when done will call the next processor in the list, using the output from the processor
4950 // at i as the argument for 'renderedRowsToProcess' on the next iteration.
4951 //
4952 // If we're at the end of the list of processors, we resolve our 'finished' callback with
4953 // the result.
4954 function startProcessor(i, renderedColumnsToProcess) {
4955 // Get the processor at 'i'
4956 var processor = self.columnsProcessors[i].processor;
4957
4958 // Call the processor, passing in the rows to process and the current columns
4959 // (note: it's wrapped in $q.when() in case the processor does not return a promise)
4960 return $q.when( processor.call(self, renderedColumnsToProcess, self.rows) )
4961 .then(function handleProcessedRows(processedColumns) {
4962 // Check for errors
4963 if (!processedColumns) {
4964 throw "Processor at index " + i + " did not return a set of renderable rows";
4965 }
4966
4967 if (!angular.isArray(processedColumns)) {
4968 throw "Processor at index " + i + " did not return an array";
4969 }
4970
4971 // Processor is done, increment the counter
4972 i++;
4973
4974 // If we're not done with the processors, call the next one
4975 if (i <= self.columnsProcessors.length - 1) {
4976 return startProcessor(i, myRenderableColumns);
4977 }
4978 // We're done! Resolve the 'finished' promise
4979 else {
4980 finished.resolve(myRenderableColumns);
4981 }
4982 });
4983 }
4984
4985 // Start on the first processor
4986 startProcessor(0, myRenderableColumns);
4987
4988 return finished.promise;
4989 };
4990
4991 Grid.prototype.setVisibleColumns = function setVisibleColumns(columns) {
4992 // gridUtil.logDebug('setVisibleColumns');
4993
4994 var self = this;
4995
4996 // Reset all the render container row caches
4997 for (var i in self.renderContainers) {
4998 var container = self.renderContainers[i];
4999
5000 container.visibleColumnCache.length = 0;
5001 }
5002
5003 for (var ci = 0; ci < columns.length; ci++) {
5004 var column = columns[ci];
5005
5006 // If the column is visible
5007 if (column.visible) {
5008 // If the column has a container specified
5009 if (typeof(column.renderContainer) !== 'undefined' && column.renderContainer) {
5010 self.renderContainers[column.renderContainer].visibleColumnCache.push(column);
5011 }
5012 // If not, put it into the body container
5013 else {
5014 self.renderContainers.body.visibleColumnCache.push(column);
5015 }
5016 }
5017 }
5018 };
5019
5020 /**
5021 * @ngdoc function
5022 * @name handleWindowResize
5023 * @methodOf ui.grid.class:Grid
5024 * @description Triggered when the browser window resizes; automatically resizes the grid
5025 */
5026 Grid.prototype.handleWindowResize = function handleWindowResize($event) {
5027 var self = this;
5028
5029 self.gridWidth = gridUtil.elementWidth(self.element);
5030 self.gridHeight = gridUtil.elementHeight(self.element);
5031
5032 self.queueRefresh();
5033 };
5034
5035 /**
5036 * @ngdoc function
5037 * @name queueRefresh
5038 * @methodOf ui.grid.class:Grid
5039 * @description queues a grid refreshCanvas, a way of debouncing all the refreshes we might otherwise issue
5040 */
5041 Grid.prototype.queueRefresh = function queueRefresh() {
5042 var self = this;
5043
5044 if (self.refreshCanceller) {
5045 $timeout.cancel(self.refreshCanceller);
5046 }
5047
5048 self.refreshCanceller = $timeout(function () {
5049 self.refreshCanvas(true);
5050 });
5051
5052 self.refreshCanceller.then(function () {
5053 self.refreshCanceller = null;
5054 });
5055
5056 return self.refreshCanceller;
5057 };
5058
5059
5060 /**
5061 * @ngdoc function
5062 * @name queueGridRefresh
5063 * @methodOf ui.grid.class:Grid
5064 * @description queues a grid refresh, a way of debouncing all the refreshes we might otherwise issue
5065 */
5066 Grid.prototype.queueGridRefresh = function queueGridRefresh() {
5067 var self = this;
5068
5069 if (self.gridRefreshCanceller) {
5070 $timeout.cancel(self.gridRefreshCanceller);
5071 }
5072
5073 self.gridRefreshCanceller = $timeout(function () {
5074 self.refresh(true);
5075 });
5076
5077 self.gridRefreshCanceller.then(function () {
5078 self.gridRefreshCanceller = null;
5079 });
5080
5081 return self.gridRefreshCanceller;
5082 };
5083
5084
5085 /**
5086 * @ngdoc function
5087 * @name updateCanvasHeight
5088 * @methodOf ui.grid.class:Grid
5089 * @description flags all render containers to update their canvas height
5090 */
5091 Grid.prototype.updateCanvasHeight = function updateCanvasHeight() {
5092 var self = this;
5093
5094 for (var containerId in self.renderContainers) {
5095 if (self.renderContainers.hasOwnProperty(containerId)) {
5096 var container = self.renderContainers[containerId];
5097 container.canvasHeightShouldUpdate = true;
5098 }
5099 }
5100 };
5101
5102 /**
5103 * @ngdoc function
5104 * @name buildStyles
5105 * @methodOf ui.grid.class:Grid
5106 * @description calls each styleComputation function
5107 */
5108 // TODO: this used to take $scope, but couldn't see that it was used
5109 Grid.prototype.buildStyles = function buildStyles() {
5110 // gridUtil.logDebug('buildStyles');
5111
5112 var self = this;
5113
5114 self.customStyles = '';
5115
5116 self.styleComputations
5117 .sort(function(a, b) {
5118 if (a.priority === null) { return 1; }
5119 if (b.priority === null) { return -1; }
5120 if (a.priority === null && b.priority === null) { return 0; }
5121 return a.priority - b.priority;
5122 })
5123 .forEach(function (compInfo) {
5124 // this used to provide $scope as a second parameter, but I couldn't find any
5125 // style builders that used it, so removed it as part of moving to grid from controller
5126 var ret = compInfo.func.call(self);
5127
5128 if (angular.isString(ret)) {
5129 self.customStyles += '\n' + ret;
5130 }
5131 });
5132 };
5133
5134
5135 Grid.prototype.minColumnsToRender = function minColumnsToRender() {
5136 var self = this;
5137 var viewport = this.getViewportWidth();
5138
5139 var min = 0;
5140 var totalWidth = 0;
5141 self.columns.forEach(function(col, i) {
5142 if (totalWidth < viewport) {
5143 totalWidth += col.drawnWidth;
5144 min++;
5145 }
5146 else {
5147 var currWidth = 0;
5148 for (var j = i; j >= i - min; j--) {
5149 currWidth += self.columns[j].drawnWidth;
5150 }
5151 if (currWidth < viewport) {
5152 min++;
5153 }
5154 }
5155 });
5156
5157 return min;
5158 };
5159
5160 Grid.prototype.getBodyHeight = function getBodyHeight() {
5161 // Start with the viewportHeight
5162 var bodyHeight = this.getViewportHeight();
5163
5164 // Add the horizontal scrollbar height if there is one
5165 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5166 // bodyHeight = bodyHeight + this.horizontalScrollbarHeight;
5167 //}
5168
5169 return bodyHeight;
5170 };
5171
5172 // NOTE: viewport drawable height is the height of the grid minus the header row height (including any border)
5173 // TODO(c0bra): account for footer height
5174 Grid.prototype.getViewportHeight = function getViewportHeight() {
5175 var self = this;
5176
5177 var viewPortHeight = this.gridHeight - this.headerHeight - this.footerHeight;
5178
5179 // Account for native horizontal scrollbar, if present
5180 //if (typeof(this.horizontalScrollbarHeight) !== 'undefined' && this.horizontalScrollbarHeight !== undefined && this.horizontalScrollbarHeight > 0) {
5181 // viewPortHeight = viewPortHeight - this.horizontalScrollbarHeight;
5182 //}
5183
5184 var adjustment = self.getViewportAdjustment();
5185
5186 viewPortHeight = viewPortHeight + adjustment.height;
5187
5188 //gridUtil.logDebug('viewPortHeight', viewPortHeight);
5189
5190 return viewPortHeight;
5191 };
5192
5193 Grid.prototype.getViewportWidth = function getViewportWidth() {
5194 var self = this;
5195
5196 var viewPortWidth = this.gridWidth;
5197
5198 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5199 // viewPortWidth = viewPortWidth - this.verticalScrollbarWidth;
5200 //}
5201
5202 var adjustment = self.getViewportAdjustment();
5203
5204 viewPortWidth = viewPortWidth + adjustment.width;
5205
5206 //gridUtil.logDebug('getviewPortWidth', viewPortWidth);
5207
5208 return viewPortWidth;
5209 };
5210
5211 Grid.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
5212 var viewPortWidth = this.getViewportWidth();
5213
5214 //if (typeof(this.verticalScrollbarWidth) !== 'undefined' && this.verticalScrollbarWidth !== undefined && this.verticalScrollbarWidth > 0) {
5215 // viewPortWidth = viewPortWidth + this.verticalScrollbarWidth;
5216 //}
5217
5218 return viewPortWidth;
5219 };
5220
5221 Grid.prototype.addVerticalScrollSync = function (containerId, callBackFn) {
5222 this.verticalScrollSyncCallBackFns[containerId] = callBackFn;
5223 };
5224
5225 Grid.prototype.addHorizontalScrollSync = function (containerId, callBackFn) {
5226 this.horizontalScrollSyncCallBackFns[containerId] = callBackFn;
5227 };
5228
5229 /**
5230 * Scroll needed containers by calling their ScrollSyncs
5231 * @param sourceContainerId the containerId that has already set it's top/left.
5232 * can be empty string which means all containers need to set top/left
5233 * @param scrollEvent
5234 */
5235 Grid.prototype.scrollContainers = function (sourceContainerId, scrollEvent) {
5236
5237 if (scrollEvent.y) {
5238 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5239 var verts = ['body','left', 'right'];
5240
5241 this.flagScrollingVertically(scrollEvent);
5242
5243 if (sourceContainerId === 'body') {
5244 verts = ['left', 'right'];
5245 }
5246 else if (sourceContainerId === 'left') {
5247 verts = ['body', 'right'];
5248 }
5249 else if (sourceContainerId === 'right') {
5250 verts = ['body', 'left'];
5251 }
5252
5253 for (var i = 0; i < verts.length; i++) {
5254 var id = verts[i];
5255 if (this.verticalScrollSyncCallBackFns[id]) {
5256 this.verticalScrollSyncCallBackFns[id](scrollEvent);
5257 }
5258 }
5259
5260 }
5261
5262 if (scrollEvent.x) {
5263 //default for no container Id (ex. mousewheel means that all containers must set scrollTop/Left)
5264 var horizs = ['body','bodyheader', 'bodyfooter'];
5265
5266 this.flagScrollingHorizontally(scrollEvent);
5267 if (sourceContainerId === 'body') {
5268 horizs = ['bodyheader', 'bodyfooter'];
5269 }
5270
5271 for (var j = 0; j < horizs.length; j++) {
5272 var idh = horizs[j];
5273 if (this.horizontalScrollSyncCallBackFns[idh]) {
5274 this.horizontalScrollSyncCallBackFns[idh](scrollEvent);
5275 }
5276 }
5277
5278 }
5279
5280 };
5281
5282 Grid.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
5283 this.viewportAdjusters.push(func);
5284 };
5285
5286 Grid.prototype.removeViewportAdjuster = function registerViewportAdjuster(func) {
5287 var idx = this.viewportAdjusters.indexOf(func);
5288
5289 if (typeof(idx) !== 'undefined' && idx !== undefined) {
5290 this.viewportAdjusters.splice(idx, 1);
5291 }
5292 };
5293
5294 Grid.prototype.getViewportAdjustment = function getViewportAdjustment() {
5295 var self = this;
5296
5297 var adjustment = { height: 0, width: 0 };
5298
5299 self.viewportAdjusters.forEach(function (func) {
5300 adjustment = func.call(this, adjustment);
5301 });
5302
5303 return adjustment;
5304 };
5305
5306 Grid.prototype.getVisibleRowCount = function getVisibleRowCount() {
5307 // var count = 0;
5308
5309 // this.rows.forEach(function (row) {
5310 // if (row.visible) {
5311 // count++;
5312 // }
5313 // });
5314
5315 // return this.visibleRowCache.length;
5316 return this.renderContainers.body.visibleRowCache.length;
5317 };
5318
5319 Grid.prototype.getVisibleRows = function getVisibleRows() {
5320 return this.renderContainers.body.visibleRowCache;
5321 };
5322
5323 Grid.prototype.getVisibleColumnCount = function getVisibleColumnCount() {
5324 // var count = 0;
5325
5326 // this.rows.forEach(function (row) {
5327 // if (row.visible) {
5328 // count++;
5329 // }
5330 // });
5331
5332 // return this.visibleRowCache.length;
5333 return this.renderContainers.body.visibleColumnCache.length;
5334 };
5335
5336
5337 Grid.prototype.searchRows = function searchRows(renderableRows) {
5338 return rowSearcher.search(this, renderableRows, this.columns);
5339 };
5340
5341 Grid.prototype.sortByColumn = function sortByColumn(renderableRows) {
5342 return rowSorter.sort(this, renderableRows, this.columns);
5343 };
5344
5345 /**
5346 * @ngdoc function
5347 * @name getCellValue
5348 * @methodOf ui.grid.class:Grid
5349 * @description Gets the value of a cell for a particular row and column
5350 * @param {GridRow} row Row to access
5351 * @param {GridColumn} col Column to access
5352 */
5353 Grid.prototype.getCellValue = function getCellValue(row, col){
5354 if ( typeof(row.entity[ '$$' + col.uid ]) !== 'undefined' ) {
5355 return row.entity[ '$$' + col.uid].rendered;
5356 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined' ){
5357 return row.entity[col.field];
5358 } else {
5359 if (!col.cellValueGetterCache) {
5360 col.cellValueGetterCache = $parse(row.getEntityQualifiedColField(col));
5361 }
5362
5363 return col.cellValueGetterCache(row);
5364 }
5365 };
5366
5367 /**
5368 * @ngdoc function
5369 * @name getCellDisplayValue
5370 * @methodOf ui.grid.class:Grid
5371 * @description Gets the displayed value of a cell after applying any the `cellFilter`
5372 * @param {GridRow} row Row to access
5373 * @param {GridColumn} col Column to access
5374 */
5375 Grid.prototype.getCellDisplayValue = function getCellDisplayValue(row, col) {
5376 if ( !col.cellDisplayGetterCache ) {
5377 var custom_filter = col.cellFilter ? " | " + col.cellFilter : "";
5378
5379 if (typeof(row.entity['$$' + col.uid]) !== 'undefined') {
5380 col.cellDisplayGetterCache = $parse(row.entity['$$' + col.uid].rendered + custom_filter);
5381 } else if (this.options.flatEntityAccess && typeof(col.field) !== 'undefined') {
5382 col.cellDisplayGetterCache = $parse(row.entity[col.field] + custom_filter);
5383 } else {
5384 col.cellDisplayGetterCache = $parse(row.getEntityQualifiedColField(col) + custom_filter);
5385 }
5386 }
5387
5388 return col.cellDisplayGetterCache(row);
5389 };
5390
5391
5392 Grid.prototype.getNextColumnSortPriority = function getNextColumnSortPriority() {
5393 var self = this,
5394 p = 0;
5395
5396 self.columns.forEach(function (col) {
5397 if (col.sort && col.sort.priority && col.sort.priority > p) {
5398 p = col.sort.priority;
5399 }
5400 });
5401
5402 return p + 1;
5403 };
5404
5405 /**
5406 * @ngdoc function
5407 * @name resetColumnSorting
5408 * @methodOf ui.grid.class:Grid
5409 * @description Return the columns that the grid is currently being sorted by
5410 * @param {GridColumn} [excludedColumn] Optional GridColumn to exclude from having its sorting reset
5411 */
5412 Grid.prototype.resetColumnSorting = function resetColumnSorting(excludeCol) {
5413 var self = this;
5414
5415 self.columns.forEach(function (col) {
5416 if (col !== excludeCol && !col.suppressRemoveSort) {
5417 col.sort = {};
5418 }
5419 });
5420 };
5421
5422 /**
5423 * @ngdoc function
5424 * @name getColumnSorting
5425 * @methodOf ui.grid.class:Grid
5426 * @description Return the columns that the grid is currently being sorted by
5427 * @returns {Array[GridColumn]} An array of GridColumn objects
5428 */
5429 Grid.prototype.getColumnSorting = function getColumnSorting() {
5430 var self = this;
5431
5432 var sortedCols = [], myCols;
5433
5434 // Iterate through all the columns, sorted by priority
5435 // Make local copy of column list, because sorting is in-place and we do not want to
5436 // change the original sequence of columns
5437 myCols = self.columns.slice(0);
5438 myCols.sort(rowSorter.prioritySort).forEach(function (col) {
5439 if (col.sort && typeof(col.sort.direction) !== 'undefined' && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
5440 sortedCols.push(col);
5441 }
5442 });
5443
5444 return sortedCols;
5445 };
5446
5447 /**
5448 * @ngdoc function
5449 * @name sortColumn
5450 * @methodOf ui.grid.class:Grid
5451 * @description Set the sorting on a given column, optionally resetting any existing sorting on the Grid.
5452 * Emits the sortChanged event whenever the sort criteria are changed.
5453 * @param {GridColumn} column Column to set the sorting on
5454 * @param {uiGridConstants.ASC|uiGridConstants.DESC} [direction] Direction to sort by, either descending or ascending.
5455 * If not provided, the column will iterate through the sort directions
5456 * specified in the {@link ui.grid.class:GridOptions.columnDef#sortDirectionCycle sortDirectionCycle} attribute.
5457 * @param {boolean} [add] Add this column to the sorting. If not provided or set to `false`, the Grid will reset any existing sorting and sort
5458 * by this column only
5459 * @returns {Promise} A resolved promise that supplies the column.
5460 */
5461
5462 Grid.prototype.sortColumn = function sortColumn(column, directionOrAdd, add) {
5463 var self = this,
5464 direction = null;
5465
5466 if (typeof(column) === 'undefined' || !column) {
5467 throw new Error('No column parameter provided');
5468 }
5469
5470 // Second argument can either be a direction or whether to add this column to the existing sort.
5471 // If it's a boolean, it's an add, otherwise, it's a direction
5472 if (typeof(directionOrAdd) === 'boolean') {
5473 add = directionOrAdd;
5474 }
5475 else {
5476 direction = directionOrAdd;
5477 }
5478
5479 if (!add) {
5480 self.resetColumnSorting(column);
5481 column.sort.priority = 0;
5482 // Get the actual priority since there may be columns which have suppressRemoveSort set
5483 column.sort.priority = self.getNextColumnSortPriority();
5484 }
5485 else if (!column.sort.priority){
5486 column.sort.priority = self.getNextColumnSortPriority();
5487 }
5488
5489 if (!direction) {
5490 // Find the current position in the cycle (or -1).
5491 var i = column.sortDirectionCycle.indexOf(column.sort.direction ? column.sort.direction : null);
5492 // Proceed to the next position in the cycle (or start at the beginning).
5493 i = (i+1) % column.sortDirectionCycle.length;
5494 // If suppressRemoveSort is set, and the next position in the cycle would
5495 // remove the sort, skip it.
5496 if (column.colDef && column.suppressRemoveSort && !column.sortDirectionCycle[i]) {
5497 i = (i+1) % column.sortDirectionCycle.length;
5498 }
5499
5500 if (column.sortDirectionCycle[i]) {
5501 column.sort.direction = column.sortDirectionCycle[i];
5502 } else {
5503 column.sort = {};
5504 }
5505 }
5506 else {
5507 column.sort.direction = direction;
5508 }
5509
5510 self.api.core.raise.sortChanged( self, self.getColumnSorting() );
5511
5512 return $q.when(column);
5513 };
5514
5515 /**
5516 * communicate to outside world that we are done with initial rendering
5517 */
5518 Grid.prototype.renderingComplete = function(){
5519 if (angular.isFunction(this.options.onRegisterApi)) {
5520 this.options.onRegisterApi(this.api);
5521 }
5522 this.api.core.raise.renderingComplete( this.api );
5523 };
5524
5525 Grid.prototype.createRowHashMap = function createRowHashMap() {
5526 var self = this;
5527
5528 var hashMap = new RowHashMap();
5529 hashMap.grid = self;
5530
5531 return hashMap;
5532 };
5533
5534
5535 /**
5536 * @ngdoc function
5537 * @name refresh
5538 * @methodOf ui.grid.class:Grid
5539 * @description Refresh the rendered grid on screen.
5540 * @param {boolean} [rowsAltered] Optional flag for refreshing when the number of rows has changed.
5541 */
5542 Grid.prototype.refresh = function refresh(rowsAltered) {
5543 var self = this;
5544
5545 var p1 = self.processRowsProcessors(self.rows).then(function (renderableRows) {
5546 self.setVisibleRows(renderableRows);
5547 });
5548
5549 var p2 = self.processColumnsProcessors(self.columns).then(function (renderableColumns) {
5550 self.setVisibleColumns(renderableColumns);
5551 });
5552
5553 return $q.all([p1, p2]).then(function () {
5554 self.redrawInPlace(rowsAltered);
5555
5556 self.refreshCanvas(true);
5557 });
5558 };
5559
5560 /**
5561 * @ngdoc function
5562 * @name refreshRows
5563 * @methodOf ui.grid.class:Grid
5564 * @description Refresh the rendered rows on screen? Note: not functional at present
5565 * @returns {promise} promise that is resolved when render completes?
5566 *
5567 */
5568 Grid.prototype.refreshRows = function refreshRows() {
5569 var self = this;
5570
5571 return self.processRowsProcessors(self.rows)
5572 .then(function (renderableRows) {
5573 self.setVisibleRows(renderableRows);
5574
5575 self.redrawInPlace();
5576
5577 self.refreshCanvas( true );
5578 });
5579 };
5580
5581 /**
5582 * @ngdoc function
5583 * @name refreshCanvas
5584 * @methodOf ui.grid.class:Grid
5585 * @description Builds all styles and recalculates much of the grid sizing
5586 * @param {object} buildStyles optional parameter. Use TBD
5587 * @returns {promise} promise that is resolved when the canvas
5588 * has been refreshed
5589 *
5590 */
5591 Grid.prototype.refreshCanvas = function(buildStyles) {
5592 var self = this;
5593
5594 if (buildStyles) {
5595 self.buildStyles();
5596 }
5597
5598 var p = $q.defer();
5599
5600 // Get all the header heights
5601 var containerHeadersToRecalc = [];
5602 for (var containerId in self.renderContainers) {
5603 if (self.renderContainers.hasOwnProperty(containerId)) {
5604 var container = self.renderContainers[containerId];
5605
5606 // Skip containers that have no canvasWidth set yet
5607 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5608 continue;
5609 }
5610
5611 if (container.header || container.headerCanvas) {
5612 container.explicitHeaderHeight = container.explicitHeaderHeight || null;
5613 container.explicitHeaderCanvasHeight = container.explicitHeaderCanvasHeight || null;
5614
5615 containerHeadersToRecalc.push(container);
5616 }
5617 }
5618 }
5619
5620 /*
5621 *
5622 * Here we loop through the headers, measuring each element as well as any header "canvas" it has within it.
5623 *
5624 * If any header is less than the largest header height, it will be resized to that so that we don't have headers
5625 * with different heights, which looks like a rendering problem
5626 *
5627 * We'll do the same thing with the header canvases, and give the header CELLS an explicit height if their canvas
5628 * is smaller than the largest canvas height. That was header cells without extra controls like filtering don't
5629 * appear shorter than other cells.
5630 *
5631 */
5632 if (containerHeadersToRecalc.length > 0) {
5633 // Build the styles without the explicit header heights
5634 if (buildStyles) {
5635 self.buildStyles();
5636 }
5637
5638 // Putting in a timeout as it's not calculating after the grid element is rendered and filled out
5639 $timeout(function() {
5640 // var oldHeaderHeight = self.grid.headerHeight;
5641 // self.grid.headerHeight = gridUtil.outerElementHeight(self.header);
5642
5643 var rebuildStyles = false;
5644
5645 // Get all the header heights
5646 var maxHeaderHeight = 0;
5647 var maxHeaderCanvasHeight = 0;
5648 var i, container;
5649 var getHeight = function(oldVal, newVal){
5650 if ( oldVal !== newVal){
5651 rebuildStyles = true;
5652 }
5653 return newVal;
5654 };
5655 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5656 container = containerHeadersToRecalc[i];
5657
5658 // Skip containers that have no canvasWidth set yet
5659 if (container.canvasWidth === null || isNaN(container.canvasWidth)) {
5660 continue;
5661 }
5662
5663 if (container.header) {
5664 var headerHeight = container.headerHeight = getHeight(container.headerHeight, parseInt(gridUtil.outerElementHeight(container.header), 10));
5665
5666 // Get the "inner" header height, that is the height minus the top and bottom borders, if present. We'll use it to make sure all the headers have a consistent height
5667 var topBorder = gridUtil.getBorderSize(container.header, 'top');
5668 var bottomBorder = gridUtil.getBorderSize(container.header, 'bottom');
5669 var innerHeaderHeight = parseInt(headerHeight - topBorder - bottomBorder, 10);
5670
5671 innerHeaderHeight = innerHeaderHeight < 0 ? 0 : innerHeaderHeight;
5672
5673 container.innerHeaderHeight = innerHeaderHeight;
5674
5675 // If the header doesn't have an explicit height set, save the largest header height for use later
5676 // Explicit header heights are based off of the max we are calculating here. We never want to base the max on something we're setting explicitly
5677 if (!container.explicitHeaderHeight && innerHeaderHeight > maxHeaderHeight) {
5678 maxHeaderHeight = innerHeaderHeight;
5679 }
5680 }
5681
5682 if (container.headerCanvas) {
5683 var headerCanvasHeight = container.headerCanvasHeight = getHeight(container.headerCanvasHeight, parseInt(gridUtil.outerElementHeight(container.headerCanvas), 10));
5684
5685
5686 // If the header doesn't have an explicit canvas height, save the largest header canvas height for use later
5687 // Explicit header heights are based off of the max we are calculating here. We never want to base the max on something we're setting explicitly
5688 if (!container.explicitHeaderCanvasHeight && headerCanvasHeight > maxHeaderCanvasHeight) {
5689 maxHeaderCanvasHeight = headerCanvasHeight;
5690 }
5691 }
5692 }
5693
5694 // Go through all the headers
5695 for (i = 0; i < containerHeadersToRecalc.length; i++) {
5696 container = containerHeadersToRecalc[i];
5697
5698 /* If:
5699 1. We have a max header height
5700 2. This container has a header height defined
5701 3. And either this container has an explicit header height set, OR its header height is less than the max
5702
5703 then:
5704
5705 Give this container's header an explicit height so it will line up with the tallest header
5706 */
5707 if (
5708 maxHeaderHeight > 0 && typeof(container.headerHeight) !== 'undefined' && container.headerHeight !== null &&
5709 (container.explicitHeaderHeight || container.headerHeight < maxHeaderHeight)
5710 ) {
5711 container.explicitHeaderHeight = getHeight(container.explicitHeaderHeight, maxHeaderHeight);
5712 }
5713
5714 // Do the same as above except for the header canvas
5715 if (
5716 maxHeaderCanvasHeight > 0 && typeof(container.headerCanvasHeight) !== 'undefined' && container.headerCanvasHeight !== null &&
5717 (container.explicitHeaderCanvasHeight || container.headerCanvasHeight < maxHeaderCanvasHeight)
5718 ) {
5719 container.explicitHeaderCanvasHeight = getHeight(container.explicitHeaderCanvasHeight, maxHeaderCanvasHeight);
5720 }
5721 }
5722
5723 // Rebuild styles if the header height has changed
5724 // The header height is used in body/viewport calculations and those are then used in other styles so we need it to be available
5725 if (buildStyles && rebuildStyles) {
5726 self.buildStyles();
5727 }
5728
5729 p.resolve();
5730 });
5731 }
5732 else {
5733 // Timeout still needs to be here to trigger digest after styles have been rebuilt
5734 $timeout(function() {
5735 p.resolve();
5736 });
5737 }
5738
5739 return p.promise;
5740 };
5741
5742
5743 /**
5744 * @ngdoc function
5745 * @name redrawCanvas
5746 * @methodOf ui.grid.class:Grid
5747 * @description Redraw the rows and columns based on our current scroll position
5748 * @param {boolean} [rowsAdded] Optional to indicate rows are added and the scroll percentage must be recalculated
5749 *
5750 */
5751 Grid.prototype.redrawInPlace = function redrawInPlace(rowsAdded) {
5752 // gridUtil.logDebug('redrawInPlace');
5753
5754 var self = this;
5755
5756 for (var i in self.renderContainers) {
5757 var container = self.renderContainers[i];
5758
5759 // gridUtil.logDebug('redrawing container', i);
5760
5761 if (rowsAdded) {
5762 container.adjustRows(container.prevScrollTop, null);
5763 container.adjustColumns(container.prevScrollLeft, null);
5764 }
5765 else {
5766 container.adjustRows(null, container.prevScrolltopPercentage);
5767 container.adjustColumns(null, container.prevScrollleftPercentage);
5768 }
5769 }
5770 };
5771
5772 /**
5773 * @ngdoc function
5774 * @name hasLeftContainerColumns
5775 * @methodOf ui.grid.class:Grid
5776 * @description returns true if leftContainer has columns
5777 */
5778 Grid.prototype.hasLeftContainerColumns = function () {
5779 return this.hasLeftContainer() && this.renderContainers.left.renderedColumns.length > 0;
5780 };
5781
5782 /**
5783 * @ngdoc function
5784 * @name hasRightContainerColumns
5785 * @methodOf ui.grid.class:Grid
5786 * @description returns true if rightContainer has columns
5787 */
5788 Grid.prototype.hasRightContainerColumns = function () {
5789 return this.hasRightContainer() && this.renderContainers.right.renderedColumns.length > 0;
5790 };
5791
5792 /**
5793 * @ngdoc method
5794 * @methodOf ui.grid.class:Grid
5795 * @name scrollToIfNecessary
5796 * @description Scrolls the grid to make a certain row and column combo visible,
5797 * in the case that it is not completely visible on the screen already.
5798 * @param {GridRow} gridRow row to make visible
5799 * @param {GridCol} gridCol column to make visible
5800 * @returns {promise} a promise that is resolved when scrolling is complete
5801 */
5802 Grid.prototype.scrollToIfNecessary = function (gridRow, gridCol) {
5803 var self = this;
5804
5805 var scrollEvent = new ScrollEvent(self, 'uiGrid.scrollToIfNecessary');
5806
5807 // Alias the visible row and column caches
5808 var visRowCache = self.renderContainers.body.visibleRowCache;
5809 var visColCache = self.renderContainers.body.visibleColumnCache;
5810
5811 /*-- Get the top, left, right, and bottom "scrolled" edges of the grid --*/
5812
5813 // The top boundary is the current Y scroll position PLUS the header height, because the header can obscure rows when the grid is scrolled downwards
5814 var topBound = self.renderContainers.body.prevScrollTop + self.headerHeight;
5815
5816 // Don't the let top boundary be less than 0
5817 topBound = (topBound < 0) ? 0 : topBound;
5818
5819 // The left boundary is the current X scroll position
5820 var leftBound = self.renderContainers.body.prevScrollLeft;
5821
5822 // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height.
5823 // Basically this is the viewport height added on to the scroll position
5824 var bottomBound = self.renderContainers.body.prevScrollTop + self.gridHeight - self.renderContainers.body.headerHeight - self.footerHeight - self.scrollbarWidth;
5825
5826 // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows
5827 //if (self.horizontalScrollbarHeight) {
5828 // bottomBound = bottomBound - self.horizontalScrollbarHeight;
5829 //}
5830
5831 // The right position is the current X scroll position minus the grid width
5832 var rightBound = self.renderContainers.body.prevScrollLeft + Math.ceil(self.renderContainers.body.getViewportWidth());
5833
5834 // If there's a vertical scrollbar, subtract it from the right boundary or we'll allow it to obscure cells
5835 //if (self.verticalScrollbarWidth) {
5836 // rightBound = rightBound - self.verticalScrollbarWidth;
5837 //}
5838
5839 // We were given a row to scroll to
5840 if (gridRow !== null) {
5841 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5842 var seekRowIndex = visRowCache.indexOf(gridRow);
5843
5844 // Total vertical scroll length of the grid
5845 var scrollLength = (self.renderContainers.body.getCanvasHeight() - self.renderContainers.body.getViewportHeight());
5846
5847 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5848 //if (self.horizontalScrollbarHeight && self.horizontalScrollbarHeight > 0) {
5849 // scrollLength = scrollLength + self.horizontalScrollbarHeight;
5850 //}
5851
5852 // This is the minimum amount of pixels we need to scroll vertical in order to see this row.
5853 var pixelsToSeeRow = ((seekRowIndex + 1) * self.options.rowHeight);
5854
5855 // Don't let the pixels required to see the row be less than zero
5856 pixelsToSeeRow = (pixelsToSeeRow < 0) ? 0 : pixelsToSeeRow;
5857
5858 var scrollPixels, percentage;
5859
5860 // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the self...
5861 if (pixelsToSeeRow < topBound) {
5862 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5863 // to get the full position we need
5864 scrollPixels = self.renderContainers.body.prevScrollTop - (topBound - pixelsToSeeRow);
5865
5866 // Turn the scroll position into a percentage and make it an argument for a scroll event
5867 percentage = scrollPixels / scrollLength;
5868 scrollEvent.y = { percentage: percentage };
5869 }
5870 // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the self...
5871 else if (pixelsToSeeRow > bottomBound) {
5872 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5873 // to get the full position we need
5874 scrollPixels = pixelsToSeeRow - bottomBound + self.renderContainers.body.prevScrollTop;
5875
5876 // Turn the scroll position into a percentage and make it an argument for a scroll event
5877 percentage = scrollPixels / scrollLength;
5878 scrollEvent.y = { percentage: percentage };
5879 }
5880 }
5881
5882 // We were given a column to scroll to
5883 if (gridCol !== null) {
5884 // This is the index of the row we want to scroll to, within the list of rows that can be visible
5885 var seekColumnIndex = visColCache.indexOf(gridCol);
5886
5887 // Total vertical scroll length of the grid
5888 var horizScrollLength = (self.renderContainers.body.getCanvasWidth() - self.renderContainers.body.getViewportWidth());
5889
5890 // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row
5891 // if (self.verticalScrollbarWidth && self.verticalScrollbarWidth > 0) {
5892 // horizScrollLength = horizScrollLength + self.verticalScrollbarWidth;
5893 // }
5894
5895 // This is the minimum amount of pixels we need to scroll vertical in order to see this column
5896 var columnLeftEdge = 0;
5897 for (var i = 0; i < seekColumnIndex; i++) {
5898 var col = visColCache[i];
5899 columnLeftEdge += col.drawnWidth;
5900 }
5901 columnLeftEdge = (columnLeftEdge < 0) ? 0 : columnLeftEdge;
5902
5903 var columnRightEdge = columnLeftEdge + gridCol.drawnWidth;
5904
5905 // Don't let the pixels required to see the column be less than zero
5906 columnRightEdge = (columnRightEdge < 0) ? 0 : columnRightEdge;
5907
5908 var horizScrollPixels, horizPercentage;
5909
5910 // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the self...
5911 if (columnLeftEdge < leftBound) {
5912 // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\
5913 // to get the full position we need
5914 horizScrollPixels = self.renderContainers.body.prevScrollLeft - (leftBound - columnLeftEdge);
5915
5916 // Turn the scroll position into a percentage and make it an argument for a scroll event
5917 horizPercentage = horizScrollPixels / horizScrollLength;
5918 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5919 scrollEvent.x = { percentage: horizPercentage };
5920 }
5921 // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the self...
5922 else if (columnRightEdge > rightBound) {
5923 // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position
5924 // to get the full position we need
5925 horizScrollPixels = columnRightEdge - rightBound + self.renderContainers.body.prevScrollLeft;
5926
5927 // Turn the scroll position into a percentage and make it an argument for a scroll event
5928 horizPercentage = horizScrollPixels / horizScrollLength;
5929 horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage;
5930 scrollEvent.x = { percentage: horizPercentage };
5931 }
5932 }
5933
5934 var deferred = $q.defer();
5935
5936 // If we need to scroll on either the x or y axes, fire a scroll event
5937 if (scrollEvent.y || scrollEvent.x) {
5938 scrollEvent.withDelay = false;
5939 self.scrollContainers('',scrollEvent);
5940 var dereg = self.api.core.on.scrollEnd(null,function() {
5941 deferred.resolve(scrollEvent);
5942 dereg();
5943 });
5944 }
5945 else {
5946 deferred.resolve();
5947 }
5948
5949 return deferred.promise;
5950 };
5951
5952 /**
5953 * @ngdoc method
5954 * @methodOf ui.grid.class:Grid
5955 * @name scrollTo
5956 * @description Scroll the grid such that the specified
5957 * row and column is in view
5958 * @param {object} rowEntity gridOptions.data[] array instance to make visible
5959 * @param {object} colDef to make visible
5960 * @returns {promise} a promise that is resolved after any scrolling is finished
5961 */
5962 Grid.prototype.scrollTo = function (rowEntity, colDef) {
5963 var gridRow = null, gridCol = null;
5964
5965 if (rowEntity !== null && typeof(rowEntity) !== 'undefined' ) {
5966 gridRow = this.getRow(rowEntity);
5967 }
5968
5969 if (colDef !== null && typeof(colDef) !== 'undefined' ) {
5970 gridCol = this.getColumn(colDef.name ? colDef.name : colDef.field);
5971 }
5972 return this.scrollToIfNecessary(gridRow, gridCol);
5973 };
5974
5975 /**
5976 * @ngdoc function
5977 * @name clearAllFilters
5978 * @methodOf ui.grid.class:Grid
5979 * @description Clears all filters and optionally refreshes the visible rows.
5980 * @param {object} refreshRows Defaults to true.
5981 * @param {object} clearConditions Defaults to false.
5982 * @param {object} clearFlags Defaults to false.
5983 * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing.
5984 */
5985 Grid.prototype.clearAllFilters = function clearAllFilters(refreshRows, clearConditions, clearFlags) {
5986 // Default `refreshRows` to true because it will be the most commonly desired behaviour.
5987 if (refreshRows === undefined) {
5988 refreshRows = true;
5989 }
5990 if (clearConditions === undefined) {
5991 clearConditions = false;
5992 }
5993 if (clearFlags === undefined) {
5994 clearFlags = false;
5995 }
5996
5997 this.columns.forEach(function(column) {
5998 column.filters.forEach(function(filter) {
5999 filter.term = undefined;
6000
6001 if (clearConditions) {
6002 filter.condition = undefined;
6003 }
6004
6005 if (clearFlags) {
6006 filter.flags = undefined;
6007 }
6008 });
6009 });
6010
6011 if (refreshRows) {
6012 return this.refreshRows();
6013 }
6014 };
6015
6016
6017 // Blatantly stolen from Angular as it isn't exposed (yet? 2.0?)
6018 function RowHashMap() {}
6019
6020 RowHashMap.prototype = {
6021 /**
6022 * Store key value pair
6023 * @param key key to store can be any type
6024 * @param value value to store can be any type
6025 */
6026 put: function(key, value) {
6027 this[this.grid.options.rowIdentity(key)] = value;
6028 },
6029
6030 /**
6031 * @param key
6032 * @returns {Object} the value for the key
6033 */
6034 get: function(key) {
6035 return this[this.grid.options.rowIdentity(key)];
6036 },
6037
6038 /**
6039 * Remove the key/value pair
6040 * @param key
6041 */
6042 remove: function(key) {
6043 var value = this[key = this.grid.options.rowIdentity(key)];
6044 delete this[key];
6045 return value;
6046 }
6047 };
6048
6049
6050
6051 return Grid;
6052
6053 }]);
6054
6055 })();
6056
6057 (function () {
6058
6059 angular.module('ui.grid')
6060 .factory('GridApi', ['$q', '$rootScope', 'gridUtil', 'uiGridConstants', 'GridRow', 'uiGridGridMenuService',
6061 function ($q, $rootScope, gridUtil, uiGridConstants, GridRow, uiGridGridMenuService) {
6062 /**
6063 * @ngdoc function
6064 * @name ui.grid.class:GridApi
6065 * @description GridApi provides the ability to register public methods events inside the grid and allow
6066 * for other components to use the api via featureName.raise.methodName and featureName.on.eventName(function(args){}.
6067 * <br/>
6068 * To listen to events, you must add a callback to gridOptions.onRegisterApi
6069 * <pre>
6070 * $scope.gridOptions.onRegisterApi = function(gridApi){
6071 * gridApi.cellNav.on.navigate($scope,function(newRowCol, oldRowCol){
6072 * $log.log('navigation event');
6073 * });
6074 * };
6075 * </pre>
6076 * @param {object} grid grid that owns api
6077 */
6078 var GridApi = function GridApi(grid) {
6079 this.grid = grid;
6080 this.listeners = [];
6081
6082 /**
6083 * @ngdoc function
6084 * @name renderingComplete
6085 * @methodOf ui.grid.core.api:PublicApi
6086 * @description Rendering is complete, called at the same
6087 * time as `onRegisterApi`, but provides a way to obtain
6088 * that same event within features without stopping end
6089 * users from getting at the onRegisterApi method.
6090 *
6091 * Included in gridApi so that it's always there - otherwise
6092 * there is still a timing problem with when a feature can
6093 * call this.
6094 *
6095 * @param {GridApi} gridApi the grid api, as normally
6096 * returned in the onRegisterApi method
6097 *
6098 * @example
6099 * <pre>
6100 * gridApi.core.on.renderingComplete( grid );
6101 * </pre>
6102 */
6103 this.registerEvent( 'core', 'renderingComplete' );
6104
6105 /**
6106 * @ngdoc event
6107 * @name filterChanged
6108 * @eventOf ui.grid.core.api:PublicApi
6109 * @description is raised after the filter is changed. The nature
6110 * of the watch expression doesn't allow notification of what changed,
6111 * so the receiver of this event will need to re-extract the filter
6112 * conditions from the columns.
6113 *
6114 */
6115 this.registerEvent( 'core', 'filterChanged' );
6116
6117 /**
6118 * @ngdoc function
6119 * @name setRowInvisible
6120 * @methodOf ui.grid.core.api:PublicApi
6121 * @description Sets an override on the row to make it always invisible,
6122 * which will override any filtering or other visibility calculations.
6123 * If the row is currently visible then sets it to invisible and calls
6124 * both grid refresh and emits the rowsVisibleChanged event
6125 * @param {object} rowEntity gridOptions.data[] array instance
6126 */
6127 this.registerMethod( 'core', 'setRowInvisible', GridRow.prototype.setRowInvisible );
6128
6129 /**
6130 * @ngdoc function
6131 * @name clearRowInvisible
6132 * @methodOf ui.grid.core.api:PublicApi
6133 * @description Clears any override on visibility for the row so that it returns to
6134 * using normal filtering and other visibility calculations.
6135 * If the row is currently invisible then sets it to visible and calls
6136 * both grid refresh and emits the rowsVisibleChanged event
6137 * TODO: if a filter is active then we can't just set it to visible?
6138 * @param {object} rowEntity gridOptions.data[] array instance
6139 */
6140 this.registerMethod( 'core', 'clearRowInvisible', GridRow.prototype.clearRowInvisible );
6141
6142 /**
6143 * @ngdoc function
6144 * @name getVisibleRows
6145 * @methodOf ui.grid.core.api:PublicApi
6146 * @description Returns all visible rows
6147 * @param {Grid} grid the grid you want to get visible rows from
6148 * @returns {array} an array of gridRow
6149 */
6150 this.registerMethod( 'core', 'getVisibleRows', this.grid.getVisibleRows );
6151
6152 /**
6153 * @ngdoc event
6154 * @name rowsVisibleChanged
6155 * @eventOf ui.grid.core.api:PublicApi
6156 * @description is raised after the rows that are visible
6157 * change. The filtering is zero-based, so it isn't possible
6158 * to say which rows changed (unlike in the selection feature).
6159 * We can plausibly know which row was changed when setRowInvisible
6160 * is called, but in that situation the user already knows which row
6161 * they changed. When a filter runs we don't know what changed,
6162 * and that is the one that would have been useful.
6163 *
6164 */
6165 this.registerEvent( 'core', 'rowsVisibleChanged' );
6166
6167 /**
6168 * @ngdoc event
6169 * @name rowsRendered
6170 * @eventOf ui.grid.core.api:PublicApi
6171 * @description is raised after the cache of visible rows is changed.
6172 */
6173 this.registerEvent( 'core', 'rowsRendered' );
6174
6175
6176 /**
6177 * @ngdoc event
6178 * @name scrollBegin
6179 * @eventOf ui.grid.core.api:PublicApi
6180 * @description is raised when scroll begins. Is throttled, so won't be raised too frequently
6181 */
6182 this.registerEvent( 'core', 'scrollBegin' );
6183
6184 /**
6185 * @ngdoc event
6186 * @name scrollEnd
6187 * @eventOf ui.grid.core.api:PublicApi
6188 * @description is raised when scroll has finished. Is throttled, so won't be raised too frequently
6189 */
6190 this.registerEvent( 'core', 'scrollEnd' );
6191
6192 /**
6193 * @ngdoc event
6194 * @name canvasHeightChanged
6195 * @eventOf ui.grid.core.api:PublicApi
6196 * @description is raised when the canvas height has changed
6197 * <br/>
6198 * arguments: oldHeight, newHeight
6199 */
6200 this.registerEvent( 'core', 'canvasHeightChanged');
6201 };
6202
6203 /**
6204 * @ngdoc function
6205 * @name ui.grid.class:suppressEvents
6206 * @methodOf ui.grid.class:GridApi
6207 * @description Used to execute a function while disabling the specified event listeners.
6208 * Disables the listenerFunctions, executes the callbackFn, and then enables
6209 * the listenerFunctions again
6210 * @param {object} listenerFuncs listenerFunc or array of listenerFuncs to suppress. These must be the same
6211 * functions that were used in the .on.eventName method
6212 * @param {object} callBackFn function to execute
6213 * @example
6214 * <pre>
6215 * var navigate = function (newRowCol, oldRowCol){
6216 * //do something on navigate
6217 * }
6218 *
6219 * gridApi.cellNav.on.navigate(scope,navigate);
6220 *
6221 *
6222 * //call the scrollTo event and suppress our navigate listener
6223 * //scrollTo will still raise the event for other listeners
6224 * gridApi.suppressEvents(navigate, function(){
6225 * gridApi.cellNav.scrollTo(aRow, aCol);
6226 * });
6227 *
6228 * </pre>
6229 */
6230 GridApi.prototype.suppressEvents = function (listenerFuncs, callBackFn) {
6231 var self = this;
6232 var listeners = angular.isArray(listenerFuncs) ? listenerFuncs : [listenerFuncs];
6233
6234 //find all registered listeners
6235 var foundListeners = self.listeners.filter(function(listener) {
6236 return listeners.some(function(l) {
6237 return listener.handler === l;
6238 });
6239 });
6240
6241 //deregister all the listeners
6242 foundListeners.forEach(function(l){
6243 l.dereg();
6244 });
6245
6246 callBackFn();
6247
6248 //reregister all the listeners
6249 foundListeners.forEach(function(l){
6250 l.dereg = registerEventWithAngular(l.eventId, l.handler, self.grid, l._this);
6251 });
6252
6253 };
6254
6255 /**
6256 * @ngdoc function
6257 * @name registerEvent
6258 * @methodOf ui.grid.class:GridApi
6259 * @description Registers a new event for the given feature. The event will get a
6260 * .raise and .on prepended to it
6261 * <br>
6262 * .raise.eventName() - takes no arguments
6263 * <br/>
6264 * <br/>
6265 * .on.eventName(scope, callBackFn, _this)
6266 * <br/>
6267 * scope - a scope reference to add a deregister call to the scopes .$on('destroy'). Scope is optional and can be a null value,
6268 * but in this case you must deregister it yourself via the returned deregister function
6269 * <br/>
6270 * callBackFn - The function to call
6271 * <br/>
6272 * _this - optional this context variable for callbackFn. If omitted, grid.api will be used for the context
6273 * <br/>
6274 * .on.eventName returns a dereg funtion that will remove the listener. It's not necessary to use it as the listener
6275 * will be removed when the scope is destroyed.
6276 * @param {string} featureName name of the feature that raises the event
6277 * @param {string} eventName name of the event
6278 */
6279 GridApi.prototype.registerEvent = function (featureName, eventName) {
6280 var self = this;
6281 if (!self[featureName]) {
6282 self[featureName] = {};
6283 }
6284
6285 var feature = self[featureName];
6286 if (!feature.on) {
6287 feature.on = {};
6288 feature.raise = {};
6289 }
6290
6291 var eventId = self.grid.id + featureName + eventName;
6292
6293 // gridUtil.logDebug('Creating raise event method ' + featureName + '.raise.' + eventName);
6294 feature.raise[eventName] = function () {
6295 $rootScope.$emit.apply($rootScope, [eventId].concat(Array.prototype.slice.call(arguments)));
6296 };
6297
6298 // gridUtil.logDebug('Creating on event method ' + featureName + '.on.' + eventName);
6299 feature.on[eventName] = function (scope, handler, _this) {
6300 if ( scope !== null && typeof(scope.$on) === 'undefined' ){
6301 gridUtil.logError('asked to listen on ' + featureName + '.on.' + eventName + ' but scope wasn\'t passed in the input parameters. It is legitimate to pass null, but you\'ve passed something else, so you probably forgot to provide scope rather than did it deliberately, not registering');
6302 return;
6303 }
6304 var deregAngularOn = registerEventWithAngular(eventId, handler, self.grid, _this);
6305
6306 //track our listener so we can turn off and on
6307 var listener = {handler: handler, dereg: deregAngularOn, eventId: eventId, scope: scope, _this:_this};
6308 self.listeners.push(listener);
6309
6310 var removeListener = function(){
6311 listener.dereg();
6312 var index = self.listeners.indexOf(listener);
6313 self.listeners.splice(index,1);
6314 };
6315
6316 //destroy tracking when scope is destroyed
6317 if (scope) {
6318 scope.$on('$destroy', function() {
6319 removeListener();
6320 });
6321 }
6322
6323
6324 return removeListener;
6325 };
6326 };
6327
6328 function registerEventWithAngular(eventId, handler, grid, _this) {
6329 return $rootScope.$on(eventId, function (event) {
6330 var args = Array.prototype.slice.call(arguments);
6331 args.splice(0, 1); //remove evt argument
6332 handler.apply(_this ? _this : grid.api, args);
6333 });
6334 }
6335
6336 /**
6337 * @ngdoc function
6338 * @name registerEventsFromObject
6339 * @methodOf ui.grid.class:GridApi
6340 * @description Registers features and events from a simple objectMap.
6341 * eventObjectMap must be in this format (multiple features allowed)
6342 * <pre>
6343 * {featureName:
6344 * {
6345 * eventNameOne:function(args){},
6346 * eventNameTwo:function(args){}
6347 * }
6348 * }
6349 * </pre>
6350 * @param {object} eventObjectMap map of feature/event names
6351 */
6352 GridApi.prototype.registerEventsFromObject = function (eventObjectMap) {
6353 var self = this;
6354 var features = [];
6355 angular.forEach(eventObjectMap, function (featProp, featPropName) {
6356 var feature = {name: featPropName, events: []};
6357 angular.forEach(featProp, function (prop, propName) {
6358 feature.events.push(propName);
6359 });
6360 features.push(feature);
6361 });
6362
6363 features.forEach(function (feature) {
6364 feature.events.forEach(function (event) {
6365 self.registerEvent(feature.name, event);
6366 });
6367 });
6368
6369 };
6370
6371 /**
6372 * @ngdoc function
6373 * @name registerMethod
6374 * @methodOf ui.grid.class:GridApi
6375 * @description Registers a new event for the given feature
6376 * @param {string} featureName name of the feature
6377 * @param {string} methodName name of the method
6378 * @param {object} callBackFn function to execute
6379 * @param {object} _this binds callBackFn 'this' to _this. Defaults to gridApi.grid
6380 */
6381 GridApi.prototype.registerMethod = function (featureName, methodName, callBackFn, _this) {
6382 if (!this[featureName]) {
6383 this[featureName] = {};
6384 }
6385
6386 var feature = this[featureName];
6387
6388 feature[methodName] = gridUtil.createBoundedWrapper(_this || this.grid, callBackFn);
6389 };
6390
6391 /**
6392 * @ngdoc function
6393 * @name registerMethodsFromObject
6394 * @methodOf ui.grid.class:GridApi
6395 * @description Registers features and methods from a simple objectMap.
6396 * eventObjectMap must be in this format (multiple features allowed)
6397 * <br>
6398 * {featureName:
6399 * {
6400 * methodNameOne:function(args){},
6401 * methodNameTwo:function(args){}
6402 * }
6403 * @param {object} eventObjectMap map of feature/event names
6404 * @param {object} _this binds this to _this for all functions. Defaults to gridApi.grid
6405 */
6406 GridApi.prototype.registerMethodsFromObject = function (methodMap, _this) {
6407 var self = this;
6408 var features = [];
6409 angular.forEach(methodMap, function (featProp, featPropName) {
6410 var feature = {name: featPropName, methods: []};
6411 angular.forEach(featProp, function (prop, propName) {
6412 feature.methods.push({name: propName, fn: prop});
6413 });
6414 features.push(feature);
6415 });
6416
6417 features.forEach(function (feature) {
6418 feature.methods.forEach(function (method) {
6419 self.registerMethod(feature.name, method.name, method.fn, _this);
6420 });
6421 });
6422
6423 };
6424
6425 return GridApi;
6426
6427 }]);
6428
6429 })();
6430
6431 (function(){
6432
6433 angular.module('ui.grid')
6434 .factory('GridColumn', ['gridUtil', 'uiGridConstants', 'i18nService', function(gridUtil, uiGridConstants, i18nService) {
6435
6436 /**
6437 * ******************************************************************************************
6438 * PaulL1: Ugly hack here in documentation. These properties are clearly properties of GridColumn,
6439 * and need to be noted as such for those extending and building ui-grid itself.
6440 * However, from an end-developer perspective, they interact with all these through columnDefs,
6441 * and they really need to be documented there. I feel like they're relatively static, and
6442 * I can't find an elegant way for ngDoc to reference to both....so I've duplicated each
6443 * comment block. Ugh.
6444 *
6445 */
6446
6447 /**
6448 * @ngdoc property
6449 * @name name
6450 * @propertyOf ui.grid.class:GridColumn
6451 * @description (mandatory) each column should have a name, although for backward
6452 * compatibility with 2.x name can be omitted if field is present
6453 *
6454 */
6455
6456 /**
6457 * @ngdoc property
6458 * @name name
6459 * @propertyOf ui.grid.class:GridOptions.columnDef
6460 * @description (mandatory) each column should have a name, although for backward
6461 * compatibility with 2.x name can be omitted if field is present
6462 *
6463 */
6464
6465 /**
6466 * @ngdoc property
6467 * @name displayName
6468 * @propertyOf ui.grid.class:GridColumn
6469 * @description Column name that will be shown in the header. If displayName is not
6470 * provided then one is generated using the name.
6471 *
6472 */
6473
6474 /**
6475 * @ngdoc property
6476 * @name displayName
6477 * @propertyOf ui.grid.class:GridOptions.columnDef
6478 * @description Column name that will be shown in the header. If displayName is not
6479 * provided then one is generated using the name.
6480 *
6481 */
6482
6483 /**
6484 * @ngdoc property
6485 * @name field
6486 * @propertyOf ui.grid.class:GridColumn
6487 * @description field must be provided if you wish to bind to a
6488 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6489 * array element. Can be a complex expression: <code>employee.address.city</code>, or can be a function: <code>employee.getFullAddress()</code>.
6490 * See the angular docs on binding expressions.
6491 *
6492 */
6493
6494 /**
6495 * @ngdoc property
6496 * @name field
6497 * @propertyOf ui.grid.class:GridOptions.columnDef
6498 * @description field must be provided if you wish to bind to a
6499 * property in the data source. Should be an angular expression that evaluates against grid.options.data
6500 * array element. Can be a complex expression: <code>employee.address.city</code>, or can be a function: <code>employee.getFullAddress()</code>. * See the angular docs on binding expressions. *
6501 */
6502
6503 /**
6504 * @ngdoc property
6505 * @name filter
6506 * @propertyOf ui.grid.class:GridColumn
6507 * @description Filter on this column.
6508 * @example
6509 * <pre>{ term: 'text', condition: uiGridConstants.filter.STARTS_WITH, placeholder: 'type to filter...', ariaLabel: 'Filter for text', flags: { caseSensitive: false }, type: uiGridConstants.filter.SELECT, [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ] }</pre>
6510 *
6511 */
6512
6513 /**
6514 * @ngdoc object
6515 * @name ui.grid.class:GridColumn
6516 * @description Represents the viewModel for each column. Any state or methods needed for a Grid Column
6517 * are defined on this prototype
6518 * @param {ColumnDef} colDef the column def to associate with this column
6519 * @param {number} uid the unique and immutable uid we'd like to allocate to this column
6520 * @param {Grid} grid the grid we'd like to create this column in
6521 */
6522 function GridColumn(colDef, uid, grid) {
6523 var self = this;
6524
6525 self.grid = grid;
6526 self.uid = uid;
6527
6528 self.updateColumnDef(colDef, true);
6529
6530 /**
6531 * @ngdoc function
6532 * @name hideColumn
6533 * @methodOf ui.grid.class:GridColumn
6534 * @description Hides the column by setting colDef.visible = false
6535 */
6536 GridColumn.prototype.hideColumn = function() {
6537 this.colDef.visible = false;
6538 };
6539
6540 self.aggregationValue = undefined;
6541
6542 // The footer cell registers to listen for the rowsRendered event, and calls this. Needed to be
6543 // in something with a scope so that the dereg would get called
6544 self.updateAggregationValue = function() {
6545
6546 // gridUtil.logDebug('getAggregationValue for Column ' + self.colDef.name);
6547
6548 /**
6549 * @ngdoc property
6550 * @name aggregationType
6551 * @propertyOf ui.grid.class:GridOptions.columnDef
6552 * @description The aggregation that you'd like to show in the columnFooter for this
6553 * column. Valid values are in uiGridConstants, and currently include `uiGridConstants.aggregationTypes.count`,
6554 * `uiGridConstants.aggregationTypes.sum`, `uiGridConstants.aggregationTypes.avg`, `uiGridConstants.aggregationTypes.min`,
6555 * `uiGridConstants.aggregationTypes.max`.
6556 *
6557 * You can also provide a function as the aggregation type, in this case your function needs to accept the full
6558 * set of visible rows, and return a value that should be shown
6559 */
6560 if (!self.aggregationType) {
6561 self.aggregationValue = undefined;
6562 return;
6563 }
6564
6565 var result = 0;
6566 var visibleRows = self.grid.getVisibleRows();
6567
6568 var cellValues = function(){
6569 var values = [];
6570 visibleRows.forEach(function (row) {
6571 var cellValue = self.grid.getCellValue(row, self);
6572 var cellNumber = Number(cellValue);
6573 if (!isNaN(cellNumber)) {
6574 values.push(cellNumber);
6575 }
6576 });
6577 return values;
6578 };
6579
6580 if (angular.isFunction(self.aggregationType)) {
6581 self.aggregationValue = self.aggregationType(visibleRows, self);
6582 }
6583 else if (self.aggregationType === uiGridConstants.aggregationTypes.count) {
6584 self.aggregationValue = self.grid.getVisibleRowCount();
6585 }
6586 else if (self.aggregationType === uiGridConstants.aggregationTypes.sum) {
6587 cellValues().forEach(function (value) {
6588 result += value;
6589 });
6590 self.aggregationValue = result;
6591 }
6592 else if (self.aggregationType === uiGridConstants.aggregationTypes.avg) {
6593 cellValues().forEach(function (value) {
6594 result += value;
6595 });
6596 result = result / cellValues().length;
6597 self.aggregationValue = result;
6598 }
6599 else if (self.aggregationType === uiGridConstants.aggregationTypes.min) {
6600 self.aggregationValue = Math.min.apply(null, cellValues());
6601 }
6602 else if (self.aggregationType === uiGridConstants.aggregationTypes.max) {
6603 self.aggregationValue = Math.max.apply(null, cellValues());
6604 }
6605 else {
6606 self.aggregationValue = '\u00A0';
6607 }
6608 };
6609
6610 // var throttledUpdateAggregationValue = gridUtil.throttle(updateAggregationValue, self.grid.options.aggregationCalcThrottle, { trailing: true, context: self.name });
6611
6612 /**
6613 * @ngdoc function
6614 * @name getAggregationValue
6615 * @methodOf ui.grid.class:GridColumn
6616 * @description gets the aggregation value based on the aggregation type for this column.
6617 * Debounced using scrollDebounce option setting
6618 */
6619 this.getAggregationValue = function() {
6620 // if (!self.grid.isScrollingVertically && !self.grid.isScrollingHorizontally) {
6621 // throttledUpdateAggregationValue();
6622 // }
6623
6624 return self.aggregationValue;
6625 };
6626 }
6627
6628
6629 /**
6630 * @ngdoc method
6631 * @methodOf ui.grid.class:GridColumn
6632 * @name setPropertyOrDefault
6633 * @description Sets a property on the column using the passed in columnDef, and
6634 * setting the defaultValue if the value cannot be found on the colDef
6635 * @param {ColumnDef} colDef the column def to look in for the property value
6636 * @param {string} propName the property name we'd like to set
6637 * @param {object} defaultValue the value to use if the colDef doesn't provide the setting
6638 */
6639 GridColumn.prototype.setPropertyOrDefault = function (colDef, propName, defaultValue) {
6640 var self = this;
6641
6642 // Use the column definition filter if we were passed it
6643 if (typeof(colDef[propName]) !== 'undefined' && colDef[propName]) {
6644 self[propName] = colDef[propName];
6645 }
6646 // Otherwise use our own if it's set
6647 else if (typeof(self[propName]) !== 'undefined') {
6648 self[propName] = self[propName];
6649 }
6650 // Default to empty object for the filter
6651 else {
6652 self[propName] = defaultValue ? defaultValue : {};
6653 }
6654 };
6655
6656
6657
6658 /**
6659 * @ngdoc property
6660 * @name width
6661 * @propertyOf ui.grid.class:GridOptions.columnDef
6662 * @description sets the column width. Can be either
6663 * a number or a percentage, or an * for auto.
6664 * @example
6665 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', width: 100},
6666 * { field: 'field2', width: '20%'},
6667 * { field: 'field3', width: '*' }]; </pre>
6668 *
6669 */
6670
6671 /**
6672 * @ngdoc property
6673 * @name minWidth
6674 * @propertyOf ui.grid.class:GridOptions.columnDef
6675 * @description sets the minimum column width. Should be a number.
6676 * @example
6677 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', minWidth: 100}]; </pre>
6678 *
6679 */
6680
6681 /**
6682 * @ngdoc property
6683 * @name maxWidth
6684 * @propertyOf ui.grid.class:GridOptions.columnDef
6685 * @description sets the maximum column width. Should be a number.
6686 * @example
6687 * <pre> $scope.gridOptions.columnDefs = [ { field: 'field1', maxWidth: 100}]; </pre>
6688 *
6689 */
6690
6691 /**
6692 * @ngdoc property
6693 * @name visible
6694 * @propertyOf ui.grid.class:GridOptions.columnDef
6695 * @description sets whether or not the column is visible
6696 * </br>Default is true
6697 * @example
6698 * <pre> $scope.gridOptions.columnDefs = [
6699 * { field: 'field1', visible: true},
6700 * { field: 'field2', visible: false }
6701 * ]; </pre>
6702 *
6703 */
6704
6705 /**
6706 * @ngdoc property
6707 * @name sort
6708 * @propertyOf ui.grid.class:GridOptions.columnDef
6709 * @description An object of sort information, attributes are:
6710 *
6711 * - direction: values are uiGridConstants.ASC or uiGridConstants.DESC
6712 * - ignoreSort: if set to true this sort is ignored (used by tree to manipulate the sort functionality)
6713 * - priority: says what order to sort the columns in (lower priority gets sorted first).
6714 * @example
6715 * <pre>
6716 * $scope.gridOptions.columnDefs = [{
6717 * field: 'field1',
6718 * sort: {
6719 * direction: uiGridConstants.ASC,
6720 * ignoreSort: true,
6721 * priority: 0
6722 * }
6723 * }];
6724 * </pre>
6725 */
6726
6727
6728 /**
6729 * @ngdoc property
6730 * @name sortingAlgorithm
6731 * @propertyOf ui.grid.class:GridOptions.columnDef
6732 * @description Algorithm to use for sorting this column. Takes 'a' and 'b' parameters
6733 * like any normal sorting function with additional 'rowA', 'rowB', and 'direction' parameters
6734 * that are the row objects and the current direction of the sort respectively.
6735 *
6736 */
6737
6738 /**
6739 * @ngdoc array
6740 * @name filters
6741 * @propertyOf ui.grid.class:GridOptions.columnDef
6742 * @description Specify multiple filter fields.
6743 * @example
6744 * <pre>$scope.gridOptions.columnDefs = [
6745 * {
6746 * field: 'field1', filters: [
6747 * {
6748 * term: 'aa',
6749 * condition: uiGridConstants.filter.STARTS_WITH,
6750 * placeholder: 'starts with...',
6751 * ariaLabel: 'Filter for field1',
6752 * flags: { caseSensitive: false },
6753 * type: uiGridConstants.filter.SELECT,
6754 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6755 * },
6756 * {
6757 * condition: uiGridConstants.filter.ENDS_WITH,
6758 * placeholder: 'ends with...'
6759 * }
6760 * ]
6761 * }
6762 * ]; </pre>
6763 *
6764 *
6765 */
6766
6767 /**
6768 * @ngdoc array
6769 * @name filters
6770 * @propertyOf ui.grid.class:GridColumn
6771 * @description Filters for this column. Includes 'term' property bound to filter input elements.
6772 * @example
6773 * <pre>[
6774 * {
6775 * term: 'foo', // ngModel for <input>
6776 * condition: uiGridConstants.filter.STARTS_WITH,
6777 * placeholder: 'starts with...',
6778 * ariaLabel: 'Filter for foo',
6779 * flags: { caseSensitive: false },
6780 * type: uiGridConstants.filter.SELECT,
6781 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ]
6782 * },
6783 * {
6784 * term: 'baz',
6785 * condition: uiGridConstants.filter.ENDS_WITH,
6786 * placeholder: 'ends with...'
6787 * }
6788 * ] </pre>
6789 *
6790 *
6791 */
6792
6793 /**
6794 * @ngdoc array
6795 * @name menuItems
6796 * @propertyOf ui.grid.class:GridOptions.columnDef
6797 * @description used to add menu items to a column. Refer to the tutorial on this
6798 * functionality. A number of settings are supported:
6799 *
6800 * - title: controls the title that is displayed in the menu
6801 * - icon: the icon shown alongside that title
6802 * - action: the method to call when the menu is clicked
6803 * - shown: a function to evaluate to determine whether or not to show the item
6804 * - active: a function to evaluate to determine whether or not the item is currently selected
6805 * - context: context to pass to the action function, available in this.context in your handler
6806 * - leaveOpen: if set to true, the menu should stay open after the action, defaults to false
6807 * @example
6808 * <pre> $scope.gridOptions.columnDefs = [
6809 * { field: 'field1', menuItems: [
6810 * {
6811 * title: 'Outer Scope Alert',
6812 * icon: 'ui-grid-icon-info-circled',
6813 * action: function($event) {
6814 * this.context.blargh(); // $scope.blargh() would work too, this is just an example
6815 * },
6816 * shown: function() { return true; },
6817 * active: function() { return true; },
6818 * context: $scope
6819 * },
6820 * {
6821 * title: 'Grid ID',
6822 * action: function() {
6823 * alert('Grid ID: ' + this.grid.id);
6824 * }
6825 * }
6826 * ] }]; </pre>
6827 *
6828 */
6829
6830 /**
6831 * @ngdoc method
6832 * @methodOf ui.grid.class:GridColumn
6833 * @name updateColumnDef
6834 * @description Moves settings from the columnDef down onto the column,
6835 * and sets properties as appropriate
6836 * @param {ColumnDef} colDef the column def to look in for the property value
6837 * @param {boolean} isNew whether the column is being newly created, if not
6838 * we're updating an existing column, and some items such as the sort shouldn't
6839 * be copied down
6840 */
6841 GridColumn.prototype.updateColumnDef = function(colDef, isNew) {
6842 var self = this;
6843
6844 self.colDef = colDef;
6845
6846 if (colDef.name === undefined) {
6847 throw new Error('colDef.name is required for column at index ' + self.grid.options.columnDefs.indexOf(colDef));
6848 }
6849
6850 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6851
6852 if (!angular.isNumber(self.width) || !self.hasCustomWidth || colDef.allowCustomWidthOverride) {
6853 var colDefWidth = colDef.width;
6854 var parseErrorMsg = "Cannot parse column width '" + colDefWidth + "' for column named '" + colDef.name + "'";
6855 self.hasCustomWidth = false;
6856
6857 if (!angular.isString(colDefWidth) && !angular.isNumber(colDefWidth)) {
6858 self.width = '*';
6859 } else if (angular.isString(colDefWidth)) {
6860 // See if it ends with a percent
6861 if (gridUtil.endsWith(colDefWidth, '%')) {
6862 // If so we should be able to parse the non-percent-sign part to a number
6863 var percentStr = colDefWidth.replace(/%/g, '');
6864 var percent = parseInt(percentStr, 10);
6865 if (isNaN(percent)) {
6866 throw new Error(parseErrorMsg);
6867 }
6868 self.width = colDefWidth;
6869 }
6870 // And see if it's a number string
6871 else if (colDefWidth.match(/^(\d+)$/)) {
6872 self.width = parseInt(colDefWidth.match(/^(\d+)$/)[1], 10);
6873 }
6874 // Otherwise it should be a string of asterisks
6875 else if (colDefWidth.match(/^\*+$/)) {
6876 self.width = colDefWidth;
6877 }
6878 // No idea, throw an Error
6879 else {
6880 throw new Error(parseErrorMsg);
6881 }
6882 }
6883 // Is a number, use it as the width
6884 else {
6885 self.width = colDefWidth;
6886 }
6887 }
6888
6889 ['minWidth', 'maxWidth'].forEach(function (name) {
6890 var minOrMaxWidth = colDef[name];
6891 var parseErrorMsg = "Cannot parse column " + name + " '" + minOrMaxWidth + "' for column named '" + colDef.name + "'";
6892
6893 if (!angular.isString(minOrMaxWidth) && !angular.isNumber(minOrMaxWidth)) {
6894 //Sets default minWidth and maxWidth values
6895 self[name] = ((name === 'minWidth') ? 30 : 9000);
6896 } else if (angular.isString(minOrMaxWidth)) {
6897 if (minOrMaxWidth.match(/^(\d+)$/)) {
6898 self[name] = parseInt(minOrMaxWidth.match(/^(\d+)$/)[1], 10);
6899 } else {
6900 throw new Error(parseErrorMsg);
6901 }
6902 } else {
6903 self[name] = minOrMaxWidth;
6904 }
6905 });
6906
6907 //use field if it is defined; name if it is not
6908 self.field = (colDef.field === undefined) ? colDef.name : colDef.field;
6909
6910 if ( typeof( self.field ) !== 'string' ){
6911 gridUtil.logError( 'Field is not a string, this is likely to break the code, Field is: ' + self.field );
6912 }
6913
6914 self.name = colDef.name;
6915
6916 // Use colDef.displayName as long as it's not undefined, otherwise default to the field name
6917 self.displayName = (colDef.displayName === undefined) ? gridUtil.readableColumnName(colDef.name) : colDef.displayName;
6918
6919 //self.originalIndex = index;
6920
6921 self.aggregationType = angular.isDefined(colDef.aggregationType) ? colDef.aggregationType : null;
6922 self.footerCellTemplate = angular.isDefined(colDef.footerCellTemplate) ? colDef.footerCellTemplate : null;
6923
6924 /**
6925 * @ngdoc property
6926 * @name cellTooltip
6927 * @propertyOf ui.grid.class:GridOptions.columnDef
6928 * @description Whether or not to show a tooltip when a user hovers over the cell.
6929 * If set to false, no tooltip. If true, the cell value is shown in the tooltip (useful
6930 * if you have long values in your cells), if a function then that function is called
6931 * passing in the row and the col `cellTooltip( row, col )`, and the return value is shown in the tooltip,
6932 * if it is a static string then displays that static string.
6933 *
6934 * Defaults to false
6935 *
6936 */
6937 if ( typeof(colDef.cellTooltip) === 'undefined' || colDef.cellTooltip === false ) {
6938 self.cellTooltip = false;
6939 } else if ( colDef.cellTooltip === true ){
6940 self.cellTooltip = function(row, col) {
6941 return self.grid.getCellValue( row, col );
6942 };
6943 } else if (typeof(colDef.cellTooltip) === 'function' ){
6944 self.cellTooltip = colDef.cellTooltip;
6945 } else {
6946 self.cellTooltip = function ( row, col ){
6947 return col.colDef.cellTooltip;
6948 };
6949 }
6950
6951 /**
6952 * @ngdoc property
6953 * @name headerTooltip
6954 * @propertyOf ui.grid.class:GridOptions.columnDef
6955 * @description Whether or not to show a tooltip when a user hovers over the header cell.
6956 * If set to false, no tooltip. If true, the displayName is shown in the tooltip (useful
6957 * if you have long values in your headers), if a function then that function is called
6958 * passing in the row and the col `headerTooltip( col )`, and the return value is shown in the tooltip,
6959 * if a static string then shows that static string.
6960 *
6961 * Defaults to false
6962 *
6963 */
6964 if ( typeof(colDef.headerTooltip) === 'undefined' || colDef.headerTooltip === false ) {
6965 self.headerTooltip = false;
6966 } else if ( colDef.headerTooltip === true ){
6967 self.headerTooltip = function(col) {
6968 return col.displayName;
6969 };
6970 } else if (typeof(colDef.headerTooltip) === 'function' ){
6971 self.headerTooltip = colDef.headerTooltip;
6972 } else {
6973 self.headerTooltip = function ( col ) {
6974 return col.colDef.headerTooltip;
6975 };
6976 }
6977
6978
6979 /**
6980 * @ngdoc property
6981 * @name footerCellClass
6982 * @propertyOf ui.grid.class:GridOptions.columnDef
6983 * @description footerCellClass can be a string specifying the class to append to a cell
6984 * or it can be a function(row,rowRenderIndex, col, colRenderIndex) that returns a class name
6985 *
6986 */
6987 self.footerCellClass = colDef.footerCellClass;
6988
6989 /**
6990 * @ngdoc property
6991 * @name cellClass
6992 * @propertyOf ui.grid.class:GridOptions.columnDef
6993 * @description cellClass can be a string specifying the class to append to a cell
6994 * or it can be a function(row,rowRenderIndex, col, colRenderIndex) that returns a class name
6995 *
6996 */
6997 self.cellClass = colDef.cellClass;
6998
6999 /**
7000 * @ngdoc property
7001 * @name headerCellClass
7002 * @propertyOf ui.grid.class:GridOptions.columnDef
7003 * @description headerCellClass can be a string specifying the class to append to a cell
7004 * or it can be a function(row,rowRenderIndex, col, colRenderIndex) that returns a class name
7005 *
7006 */
7007 self.headerCellClass = colDef.headerCellClass;
7008
7009 /**
7010 * @ngdoc property
7011 * @name cellFilter
7012 * @propertyOf ui.grid.class:GridOptions.columnDef
7013 * @description cellFilter is a filter to apply to the content of each cell
7014 * @example
7015 * <pre>
7016 * gridOptions.columnDefs[0].cellFilter = 'date'
7017 *
7018 */
7019 self.cellFilter = colDef.cellFilter ? colDef.cellFilter : "";
7020
7021 /**
7022 * @ngdoc boolean
7023 * @name sortCellFiltered
7024 * @propertyOf ui.grid.class:GridOptions.columnDef
7025 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7026 * sorting the data. Note that when using this option uiGrid will assume that the displayed value is
7027 * a string, and use the {@link ui.grid.class:RowSorter#sortAlpha sortAlpha} `sortFn`. It is possible
7028 * to return a non-string value from an angularjs filter, in which case you should define a {@link ui.grid.class:GridOptions.columnDef#sortingAlgorithm sortingAlgorithm}
7029 * for the column which hanldes the returned type. You may specify one of the `sortingAlgorithms`
7030 * found in the {@link ui.grid.RowSorter rowSorter} service.
7031 */
7032 self.sortCellFiltered = colDef.sortCellFiltered ? true : false;
7033
7034 /**
7035 * @ngdoc boolean
7036 * @name filterCellFiltered
7037 * @propertyOf ui.grid.class:GridOptions.columnDef
7038 * @description (optional) False by default. When `true` uiGrid will apply the cellFilter before
7039 * applying "search" `filters`.
7040 */
7041 self.filterCellFiltered = colDef.filterCellFiltered ? true : false;
7042
7043 /**
7044 * @ngdoc property
7045 * @name headerCellFilter
7046 * @propertyOf ui.grid.class:GridOptions.columnDef
7047 * @description headerCellFilter is a filter to apply to the content of the column header
7048 * @example
7049 * <pre>
7050 * gridOptions.columnDefs[0].headerCellFilter = 'translate'
7051 *
7052 */
7053 self.headerCellFilter = colDef.headerCellFilter ? colDef.headerCellFilter : "";
7054
7055 /**
7056 * @ngdoc property
7057 * @name footerCellFilter
7058 * @propertyOf ui.grid.class:GridOptions.columnDef
7059 * @description footerCellFilter is a filter to apply to the content of the column footer
7060 * @example
7061 * <pre>
7062 * gridOptions.columnDefs[0].footerCellFilter = 'date'
7063 *
7064 */
7065 self.footerCellFilter = colDef.footerCellFilter ? colDef.footerCellFilter : "";
7066
7067 self.visible = gridUtil.isNullOrUndefined(colDef.visible) || colDef.visible;
7068
7069 self.headerClass = colDef.headerClass;
7070 //self.cursor = self.sortable ? 'pointer' : 'default';
7071
7072 // Turn on sorting by default
7073 self.enableSorting = typeof(colDef.enableSorting) !== 'undefined' ? colDef.enableSorting : true;
7074 self.sortingAlgorithm = colDef.sortingAlgorithm;
7075
7076 /**
7077 * @ngdoc property
7078 * @name sortDirectionCycle
7079 * @propertyOf ui.grid.class:GridOptions.columnDef
7080 * @description (optional) An array of sort directions, specifying the order that they
7081 * should cycle through as the user repeatedly clicks on the column heading.
7082 * The default is `[null, uiGridConstants.ASC, uiGridConstants.DESC]`. Null
7083 * refers to the unsorted state. This does not affect the initial sort
7084 * direction; use the {@link ui.grid.class:GridOptions.columnDef#sort sort}
7085 * property for that. If
7086 * {@link ui.grid.class:GridOptions.columnDef#suppressRemoveSort suppressRemoveSort}
7087 * is also set, the unsorted state will be skipped even if it is listed here.
7088 * Each direction may not appear in the list more than once (e.g. `[ASC,
7089 * DESC, DESC]` is not allowed), and the list may not be empty.
7090 */
7091 self.sortDirectionCycle = typeof(colDef.sortDirectionCycle) !== 'undefined' ?
7092 colDef.sortDirectionCycle :
7093 [null, uiGridConstants.ASC, uiGridConstants.DESC];
7094
7095 /**
7096 * @ngdoc boolean
7097 * @name suppressRemoveSort
7098 * @propertyOf ui.grid.class:GridOptions.columnDef
7099 * @description (optional) False by default. When enabled, this setting hides the removeSort option
7100 * in the menu, and prevents users from manually removing the sort
7101 */
7102 if ( typeof(self.suppressRemoveSort) === 'undefined'){
7103 self.suppressRemoveSort = typeof(colDef.suppressRemoveSort) !== 'undefined' ? colDef.suppressRemoveSort : false;
7104 }
7105
7106 /**
7107 * @ngdoc property
7108 * @name enableFiltering
7109 * @propertyOf ui.grid.class:GridOptions.columnDef
7110 * @description turn off filtering for an individual column, where
7111 * you've turned on filtering for the overall grid
7112 * @example
7113 * <pre>
7114 * gridOptions.columnDefs[0].enableFiltering = false;
7115 *
7116 */
7117 // Turn on filtering by default (it's disabled by default at the Grid level)
7118 self.enableFiltering = typeof(colDef.enableFiltering) !== 'undefined' ? colDef.enableFiltering : true;
7119
7120 // self.menuItems = colDef.menuItems;
7121 self.setPropertyOrDefault(colDef, 'menuItems', []);
7122
7123 // Use the column definition sort if we were passed it, but only if this is a newly added column
7124 if ( isNew ){
7125 self.setPropertyOrDefault(colDef, 'sort');
7126 }
7127
7128 // Set up default filters array for when one is not provided.
7129 // In other words, this (in column def):
7130 //
7131 // filter: { term: 'something', flags: {}, condition: [CONDITION] }
7132 //
7133 // is just shorthand for this:
7134 //
7135 // filters: [{ term: 'something', flags: {}, condition: [CONDITION] }]
7136 //
7137 var defaultFilters = [];
7138 if (colDef.filter) {
7139 defaultFilters.push(colDef.filter);
7140 }
7141 else if ( colDef.filters ){
7142 defaultFilters = colDef.filters;
7143 } else {
7144 // Add an empty filter definition object, which will
7145 // translate to a guessed condition and no pre-populated
7146 // value for the filter <input>.
7147 defaultFilters.push({});
7148 }
7149
7150 /**
7151 * @ngdoc property
7152 * @name filter
7153 * @propertyOf ui.grid.class:GridOptions.columnDef
7154 * @description Specify a single filter field on this column.
7155 *
7156 * A filter consists of a condition, a term, and a placeholder:
7157 *
7158 * - condition defines how rows are chosen as matching the filter term. This can be set to
7159 * one of the constants in uiGridConstants.filter, or you can supply a custom filter function
7160 * that gets passed the following arguments: [searchTerm, cellValue, row, column].
7161 * - term: If set, the filter field will be pre-populated
7162 * with this value.
7163 * - placeholder: String that will be set to the `<input>.placeholder` attribute.
7164 * - ariaLabel: String that will be set to the `<input>.ariaLabel` attribute. This is what is read as a label to screen reader users.
7165 * - noTerm: set this to true if you have defined a custom function in condition, and
7166 * your custom function doesn't require a term (so it can run even when the term is null)
7167 * - flags: only flag currently available is `caseSensitive`, set to false if you don't want
7168 * case sensitive matching
7169 * - type: defaults to uiGridConstants.filter.INPUT, which gives a text box. If set to uiGridConstants.filter.SELECT
7170 * then a select box will be shown with options selectOptions
7171 * - selectOptions: options in the format `[ { value: 1, label: 'male' }]`. No i18n filter is provided, you need
7172 * to perform the i18n on the values before you provide them
7173 * - disableCancelFilterButton: defaults to false. If set to true then the 'x' button that cancels/clears the filter
7174 * will not be shown.
7175 * @example
7176 * <pre>$scope.gridOptions.columnDefs = [
7177 * {
7178 * field: 'field1',
7179 * filter: {
7180 * term: 'xx',
7181 * condition: uiGridConstants.filter.STARTS_WITH,
7182 * placeholder: 'starts with...',
7183 * ariaLabel: 'Starts with filter for field1',
7184 * flags: { caseSensitive: false },
7185 * type: uiGridConstants.filter.SELECT,
7186 * selectOptions: [ { value: 1, label: 'male' }, { value: 2, label: 'female' } ],
7187 * disableCancelFilterButton: true
7188 * }
7189 * }
7190 * ]; </pre>
7191 *
7192 */
7193
7194 /*
7195
7196
7197 /*
7198
7199 self.filters = [
7200 {
7201 term: 'search term'
7202 condition: uiGridConstants.filter.CONTAINS,
7203 placeholder: 'my placeholder',
7204 ariaLabel: 'Starts with filter for field1',
7205 flags: {
7206 caseSensitive: true
7207 }
7208 }
7209 ]
7210
7211 */
7212
7213 // Only set filter if this is a newly added column, if we're updating an existing
7214 // column then we don't want to put the default filter back if the user may have already
7215 // removed it.
7216 // However, we do want to keep the settings if they change, just not the term
7217 if ( isNew ) {
7218 self.setPropertyOrDefault(colDef, 'filter');
7219 self.setPropertyOrDefault(colDef, 'filters', defaultFilters);
7220 } else if ( self.filters.length === defaultFilters.length ) {
7221 self.filters.forEach( function( filter, index ){
7222 if (typeof(defaultFilters[index].placeholder) !== 'undefined') {
7223 filter.placeholder = defaultFilters[index].placeholder;
7224 }
7225 if (typeof(defaultFilters[index].ariaLabel) !== 'undefined') {
7226 filter.ariaLabel = defaultFilters[index].ariaLabel;
7227 }
7228 if (typeof(defaultFilters[index].flags) !== 'undefined') {
7229 filter.flags = defaultFilters[index].flags;
7230 }
7231 if (typeof(defaultFilters[index].type) !== 'undefined') {
7232 filter.type = defaultFilters[index].type;
7233 }
7234 if (typeof(defaultFilters[index].selectOptions) !== 'undefined') {
7235 filter.selectOptions = defaultFilters[index].selectOptions;
7236 }
7237 });
7238 }
7239
7240 // Remove this column from the grid sorting, include inside build columns so has
7241 // access to self - all seems a bit dodgy but doesn't work otherwise so have left
7242 // as is
7243 GridColumn.prototype.unsort = function () {
7244 this.sort = {};
7245 self.grid.api.core.raise.sortChanged( self.grid, self.grid.getColumnSorting() );
7246 };
7247
7248 };
7249
7250
7251 /**
7252 * @ngdoc function
7253 * @name getColClass
7254 * @methodOf ui.grid.class:GridColumn
7255 * @description Returns the class name for the column
7256 * @param {bool} prefixDot if true, will return .className instead of className
7257 */
7258 GridColumn.prototype.getColClass = function (prefixDot) {
7259 var cls = uiGridConstants.COL_CLASS_PREFIX + this.uid;
7260
7261 return prefixDot ? '.' + cls : cls;
7262 };
7263
7264 /**
7265 * @ngdoc function
7266 * @name isPinnedLeft
7267 * @methodOf ui.grid.class:GridColumn
7268 * @description Returns true if column is in the left render container
7269 */
7270 GridColumn.prototype.isPinnedLeft = function () {
7271 return this.renderContainer === 'left';
7272 };
7273
7274 /**
7275 * @ngdoc function
7276 * @name isPinnedRight
7277 * @methodOf ui.grid.class:GridColumn
7278 * @description Returns true if column is in the right render container
7279 */
7280 GridColumn.prototype.isPinnedRight = function () {
7281 return this.renderContainer === 'right';
7282 };
7283
7284
7285 /**
7286 * @ngdoc function
7287 * @name getColClassDefinition
7288 * @methodOf ui.grid.class:GridColumn
7289 * @description Returns the class definition for th column
7290 */
7291 GridColumn.prototype.getColClassDefinition = function () {
7292 return ' .grid' + this.grid.id + ' ' + this.getColClass(true) + ' { min-width: ' + this.drawnWidth + 'px; max-width: ' + this.drawnWidth + 'px; }';
7293 };
7294
7295 /**
7296 * @ngdoc function
7297 * @name getRenderContainer
7298 * @methodOf ui.grid.class:GridColumn
7299 * @description Returns the render container object that this column belongs to.
7300 *
7301 * Columns will be default be in the `body` render container if they aren't allocated to one specifically.
7302 */
7303 GridColumn.prototype.getRenderContainer = function getRenderContainer() {
7304 var self = this;
7305
7306 var containerId = self.renderContainer;
7307
7308 if (containerId === null || containerId === '' || containerId === undefined) {
7309 containerId = 'body';
7310 }
7311
7312 return self.grid.renderContainers[containerId];
7313 };
7314
7315 /**
7316 * @ngdoc function
7317 * @name showColumn
7318 * @methodOf ui.grid.class:GridColumn
7319 * @description Makes the column visible by setting colDef.visible = true
7320 */
7321 GridColumn.prototype.showColumn = function() {
7322 this.colDef.visible = true;
7323 };
7324
7325
7326 /**
7327 * @ngdoc property
7328 * @name aggregationHideLabel
7329 * @propertyOf ui.grid.class:GridOptions.columnDef
7330 * @description defaults to false, if set to true hides the label text
7331 * in the aggregation footer, so only the value is displayed.
7332 *
7333 */
7334 /**
7335 * @ngdoc function
7336 * @name getAggregationText
7337 * @methodOf ui.grid.class:GridColumn
7338 * @description Gets the aggregation label from colDef.aggregationLabel if
7339 * specified or by using i18n, including deciding whether or not to display
7340 * based on colDef.aggregationHideLabel.
7341 *
7342 * @param {string} label the i18n lookup value to use for the column label
7343 *
7344 */
7345 GridColumn.prototype.getAggregationText = function () {
7346 var self = this;
7347 if ( self.colDef.aggregationHideLabel ){
7348 return '';
7349 }
7350 else if ( self.colDef.aggregationLabel ) {
7351 return self.colDef.aggregationLabel;
7352 }
7353 else {
7354 switch ( self.colDef.aggregationType ){
7355 case uiGridConstants.aggregationTypes.count:
7356 return i18nService.getSafeText('aggregation.count');
7357 case uiGridConstants.aggregationTypes.sum:
7358 return i18nService.getSafeText('aggregation.sum');
7359 case uiGridConstants.aggregationTypes.avg:
7360 return i18nService.getSafeText('aggregation.avg');
7361 case uiGridConstants.aggregationTypes.min:
7362 return i18nService.getSafeText('aggregation.min');
7363 case uiGridConstants.aggregationTypes.max:
7364 return i18nService.getSafeText('aggregation.max');
7365 default:
7366 return '';
7367 }
7368 }
7369 };
7370
7371 GridColumn.prototype.getCellTemplate = function () {
7372 var self = this;
7373
7374 return self.cellTemplatePromise;
7375 };
7376
7377 GridColumn.prototype.getCompiledElementFn = function () {
7378 var self = this;
7379
7380 return self.compiledElementFnDefer.promise;
7381 };
7382
7383 return GridColumn;
7384 }]);
7385
7386 })();
7387
7388 (function(){
7389
7390 angular.module('ui.grid')
7391 .factory('GridOptions', ['gridUtil','uiGridConstants', function(gridUtil,uiGridConstants) {
7392
7393 /**
7394 * @ngdoc function
7395 * @name ui.grid.class:GridOptions
7396 * @description Default GridOptions class. GridOptions are defined by the application developer and overlaid
7397 * over this object. Setting gridOptions within your controller is the most common method for an application
7398 * developer to configure the behaviour of their ui-grid
7399 *
7400 * @example To define your gridOptions within your controller:
7401 * <pre>$scope.gridOptions = {
7402 * data: $scope.myData,
7403 * columnDefs: [
7404 * { name: 'field1', displayName: 'pretty display name' },
7405 * { name: 'field2', visible: false }
7406 * ]
7407 * };</pre>
7408 *
7409 * You can then use this within your html template, when you define your grid:
7410 * <pre>&lt;div ui-grid="gridOptions"&gt;&lt;/div&gt;</pre>
7411 *
7412 * To provide default options for all of the grids within your application, use an angular
7413 * decorator to modify the GridOptions factory.
7414 * <pre>
7415 * app.config(function($provide){
7416 * $provide.decorator('GridOptions',function($delegate){
7417 * var gridOptions;
7418 * gridOptions = angular.copy($delegate);
7419 * gridOptions.initialize = function(options) {
7420 * var initOptions;
7421 * initOptions = $delegate.initialize(options);
7422 * initOptions.enableColumnMenus = false;
7423 * return initOptions;
7424 * };
7425 * return gridOptions;
7426 * });
7427 * });
7428 * </pre>
7429 */
7430 return {
7431 initialize: function( baseOptions ){
7432 /**
7433 * @ngdoc function
7434 * @name onRegisterApi
7435 * @propertyOf ui.grid.class:GridOptions
7436 * @description A callback that returns the gridApi once the grid is instantiated, which is
7437 * then used to interact with the grid programatically.
7438 *
7439 * Note that the gridApi.core.renderingComplete event is identical to this
7440 * callback, but has the advantage that it can be called from multiple places
7441 * if needed
7442 *
7443 * @example
7444 * <pre>
7445 * $scope.gridOptions.onRegisterApi = function ( gridApi ) {
7446 * $scope.gridApi = gridApi;
7447 * $scope.gridApi.selection.selectAllRows( $scope.gridApi.grid );
7448 * };
7449 * </pre>
7450 *
7451 */
7452 baseOptions.onRegisterApi = baseOptions.onRegisterApi || angular.noop();
7453
7454 /**
7455 * @ngdoc object
7456 * @name data
7457 * @propertyOf ui.grid.class:GridOptions
7458 * @description (mandatory) Array of data to be rendered into the grid, providing the data source or data binding for
7459 * the grid.
7460 *
7461 * Most commonly the data is an array of objects, where each object has a number of attributes.
7462 * Each attribute automatically becomes a column in your grid. This array could, for example, be sourced from
7463 * an angularJS $resource query request. The array can also contain complex objects, refer the binding tutorial
7464 * for examples of that.
7465 *
7466 * The most flexible usage is to set your data on $scope:
7467 *
7468 * `$scope.data = data;`
7469 *
7470 * And then direct the grid to resolve whatever is in $scope.data:
7471 *
7472 * `$scope.gridOptions.data = 'data';`
7473 *
7474 * This is the most flexible approach as it allows you to replace $scope.data whenever you feel like it without
7475 * getting pointer issues.
7476 *
7477 * Alternatively you can directly set the data array:
7478 *
7479 * `$scope.gridOptions.data = [ ];`
7480 * or
7481 *
7482 * `$http.get('/data/100.json')
7483 * .success(function(data) {
7484 * $scope.myData = data;
7485 * $scope.gridOptions.data = $scope.myData;
7486 * });`
7487 *
7488 * Where you do this, you need to take care in updating the data - you can't just update `$scope.myData` to some other
7489 * array, you need to update $scope.gridOptions.data to point to that new array as well.
7490 *
7491 */
7492 baseOptions.data = baseOptions.data || [];
7493
7494 /**
7495 * @ngdoc array
7496 * @name columnDefs
7497 * @propertyOf ui.grid.class:GridOptions
7498 * @description Array of columnDef objects. Only required property is name.
7499 * The individual options available in columnDefs are documented in the
7500 * {@link ui.grid.class:GridOptions.columnDef columnDef} section
7501 * </br>_field property can be used in place of name for backwards compatibility with 2.x_
7502 * @example
7503 *
7504 * <pre>var columnDefs = [{name:'field1'}, {name:'field2'}];</pre>
7505 *
7506 */
7507 baseOptions.columnDefs = baseOptions.columnDefs || [];
7508
7509 /**
7510 * @ngdoc object
7511 * @name ui.grid.class:GridOptions.columnDef
7512 * @description Definition / configuration of an individual column, which would typically be
7513 * one of many column definitions within the gridOptions.columnDefs array
7514 * @example
7515 * <pre>{name:'field1', field: 'field1', filter: { term: 'xxx' }}</pre>
7516 *
7517 */
7518
7519
7520 /**
7521 * @ngdoc array
7522 * @name excludeProperties
7523 * @propertyOf ui.grid.class:GridOptions
7524 * @description Array of property names in data to ignore when auto-generating column names. Provides the
7525 * inverse of columnDefs - columnDefs is a list of columns to include, excludeProperties is a list of columns
7526 * to exclude.
7527 *
7528 * If columnDefs is defined, this will be ignored.
7529 *
7530 * Defaults to ['$$hashKey']
7531 */
7532
7533 baseOptions.excludeProperties = baseOptions.excludeProperties || ['$$hashKey'];
7534
7535 /**
7536 * @ngdoc boolean
7537 * @name enableRowHashing
7538 * @propertyOf ui.grid.class:GridOptions
7539 * @description True by default. When enabled, this setting allows uiGrid to add
7540 * `$$hashKey`-type properties (similar to Angular) to elements in the `data` array. This allows
7541 * the grid to maintain state while vastly speeding up the process of altering `data` by adding/moving/removing rows.
7542 *
7543 * Note that this DOES add properties to your data that you may not want, but they are stripped out when using `angular.toJson()`. IF
7544 * you do not want this at all you can disable this setting but you will take a performance hit if you are using large numbers of rows
7545 * and are altering the data set often.
7546 */
7547 baseOptions.enableRowHashing = baseOptions.enableRowHashing !== false;
7548
7549 /**
7550 * @ngdoc function
7551 * @name rowIdentity
7552 * @methodOf ui.grid.class:GridOptions
7553 * @description This function is used to get and, if necessary, set the value uniquely identifying this row (i.e. if an identity is not present it will set one).
7554 *
7555 * By default it returns the `$$hashKey` property if it exists. If it doesn't it uses gridUtil.nextUid() to generate one
7556 */
7557 baseOptions.rowIdentity = baseOptions.rowIdentity || function rowIdentity(row) {
7558 return gridUtil.hashKey(row);
7559 };
7560
7561 /**
7562 * @ngdoc function
7563 * @name getRowIdentity
7564 * @methodOf ui.grid.class:GridOptions
7565 * @description This function returns the identity value uniquely identifying this row, if one is not present it does not set it.
7566 *
7567 * By default it returns the `$$hashKey` property but can be overridden to use any property or set of properties you want.
7568 */
7569 baseOptions.getRowIdentity = baseOptions.getRowIdentity || function getRowIdentity(row) {
7570 return row.$$hashKey;
7571 };
7572
7573 /**
7574 * @ngdoc property
7575 * @name flatEntityAccess
7576 * @propertyOf ui.grid.class:GridOptions
7577 * @description Set to true if your columns are all related directly to fields in a flat object structure - i.e.
7578 * each of your columns associate directly with a property on each of the entities in your data array.
7579 *
7580 * In that situation we can avoid all the logic associated with complex binding to functions or to properties of sub-objects,
7581 * which can provide a significant speed improvement with large data sets when filtering or sorting.
7582 *
7583 * By default false
7584 */
7585 baseOptions.flatEntityAccess = baseOptions.flatEntityAccess === true;
7586
7587 /**
7588 * @ngdoc property
7589 * @name showHeader
7590 * @propertyOf ui.grid.class:GridOptions
7591 * @description True by default. When set to false, this setting will replace the
7592 * standard header template with '<div></div>', resulting in no header being shown.
7593 */
7594 baseOptions.showHeader = typeof(baseOptions.showHeader) !== "undefined" ? baseOptions.showHeader : true;
7595
7596 /* (NOTE): Don't show this in the docs. We only use it internally
7597 * @ngdoc property
7598 * @name headerRowHeight
7599 * @propertyOf ui.grid.class:GridOptions
7600 * @description The height of the header in pixels, defaults to 30
7601 *
7602 */
7603 if (!baseOptions.showHeader) {
7604 baseOptions.headerRowHeight = 0;
7605 }
7606 else {
7607 baseOptions.headerRowHeight = typeof(baseOptions.headerRowHeight) !== "undefined" ? baseOptions.headerRowHeight : 30;
7608 }
7609
7610 /**
7611 * @ngdoc property
7612 * @name rowHeight
7613 * @propertyOf ui.grid.class:GridOptions
7614 * @description The height of the row in pixels, defaults to 30
7615 *
7616 */
7617 baseOptions.rowHeight = baseOptions.rowHeight || 30;
7618
7619 /**
7620 * @ngdoc integer
7621 * @name minRowsToShow
7622 * @propertyOf ui.grid.class:GridOptions
7623 * @description Minimum number of rows to show when the grid doesn't have a defined height. Defaults to "10".
7624 */
7625 baseOptions.minRowsToShow = typeof(baseOptions.minRowsToShow) !== "undefined" ? baseOptions.minRowsToShow : 10;
7626
7627 /**
7628 * @ngdoc property
7629 * @name showGridFooter
7630 * @propertyOf ui.grid.class:GridOptions
7631 * @description Whether or not to show the footer, defaults to false
7632 * The footer display Total Rows and Visible Rows (filtered rows)
7633 */
7634 baseOptions.showGridFooter = baseOptions.showGridFooter === true;
7635
7636 /**
7637 * @ngdoc property
7638 * @name showColumnFooter
7639 * @propertyOf ui.grid.class:GridOptions
7640 * @description Whether or not to show the column footer, defaults to false
7641 * The column footer displays column aggregates
7642 */
7643 baseOptions.showColumnFooter = baseOptions.showColumnFooter === true;
7644
7645 /**
7646 * @ngdoc property
7647 * @name columnFooterHeight
7648 * @propertyOf ui.grid.class:GridOptions
7649 * @description The height of the footer rows (column footer and grid footer) in pixels
7650 *
7651 */
7652 baseOptions.columnFooterHeight = typeof(baseOptions.columnFooterHeight) !== "undefined" ? baseOptions.columnFooterHeight : 30;
7653 baseOptions.gridFooterHeight = typeof(baseOptions.gridFooterHeight) !== "undefined" ? baseOptions.gridFooterHeight : 30;
7654
7655 baseOptions.columnWidth = typeof(baseOptions.columnWidth) !== "undefined" ? baseOptions.columnWidth : 50;
7656
7657 /**
7658 * @ngdoc property
7659 * @name maxVisibleColumnCount
7660 * @propertyOf ui.grid.class:GridOptions
7661 * @description Defaults to 200
7662 *
7663 */
7664 baseOptions.maxVisibleColumnCount = typeof(baseOptions.maxVisibleColumnCount) !== "undefined" ? baseOptions.maxVisibleColumnCount : 200;
7665
7666 /**
7667 * @ngdoc property
7668 * @name virtualizationThreshold
7669 * @propertyOf ui.grid.class:GridOptions
7670 * @description Turn virtualization on when number of data elements goes over this number, defaults to 20
7671 */
7672 baseOptions.virtualizationThreshold = typeof(baseOptions.virtualizationThreshold) !== "undefined" ? baseOptions.virtualizationThreshold : 20;
7673
7674 /**
7675 * @ngdoc property
7676 * @name columnVirtualizationThreshold
7677 * @propertyOf ui.grid.class:GridOptions
7678 * @description Turn virtualization on when number of columns goes over this number, defaults to 10
7679 */
7680 baseOptions.columnVirtualizationThreshold = typeof(baseOptions.columnVirtualizationThreshold) !== "undefined" ? baseOptions.columnVirtualizationThreshold : 10;
7681
7682 /**
7683 * @ngdoc property
7684 * @name excessRows
7685 * @propertyOf ui.grid.class:GridOptions
7686 * @description Extra rows to to render outside of the viewport, which helps with smoothness of scrolling.
7687 * Defaults to 4
7688 */
7689 baseOptions.excessRows = typeof(baseOptions.excessRows) !== "undefined" ? baseOptions.excessRows : 4;
7690 /**
7691 * @ngdoc property
7692 * @name scrollThreshold
7693 * @propertyOf ui.grid.class:GridOptions
7694 * @description Defaults to 4
7695 */
7696 baseOptions.scrollThreshold = typeof(baseOptions.scrollThreshold) !== "undefined" ? baseOptions.scrollThreshold : 4;
7697
7698 /**
7699 * @ngdoc property
7700 * @name excessColumns
7701 * @propertyOf ui.grid.class:GridOptions
7702 * @description Extra columns to to render outside of the viewport, which helps with smoothness of scrolling.
7703 * Defaults to 4
7704 */
7705 baseOptions.excessColumns = typeof(baseOptions.excessColumns) !== "undefined" ? baseOptions.excessColumns : 4;
7706 /**
7707 * @ngdoc property
7708 * @name horizontalScrollThreshold
7709 * @propertyOf ui.grid.class:GridOptions
7710 * @description Defaults to 4
7711 */
7712 baseOptions.horizontalScrollThreshold = typeof(baseOptions.horizontalScrollThreshold) !== "undefined" ? baseOptions.horizontalScrollThreshold : 2;
7713
7714
7715 /**
7716 * @ngdoc property
7717 * @name aggregationCalcThrottle
7718 * @propertyOf ui.grid.class:GridOptions
7719 * @description Default time in milliseconds to throttle aggregation calcuations, defaults to 500ms
7720 */
7721 baseOptions.aggregationCalcThrottle = typeof(baseOptions.aggregationCalcThrottle) !== "undefined" ? baseOptions.aggregationCalcThrottle : 500;
7722
7723 /**
7724 * @ngdoc property
7725 * @name wheelScrollThrottle
7726 * @propertyOf ui.grid.class:GridOptions
7727 * @description Default time in milliseconds to throttle scroll events to, defaults to 70ms
7728 */
7729 baseOptions.wheelScrollThrottle = typeof(baseOptions.wheelScrollThrottle) !== "undefined" ? baseOptions.wheelScrollThrottle : 70;
7730
7731
7732 /**
7733 * @ngdoc property
7734 * @name scrollDebounce
7735 * @propertyOf ui.grid.class:GridOptions
7736 * @description Default time in milliseconds to debounce scroll events, defaults to 300ms
7737 */
7738 baseOptions.scrollDebounce = typeof(baseOptions.scrollDebounce) !== "undefined" ? baseOptions.scrollDebounce : 300;
7739
7740 /**
7741 * @ngdoc boolean
7742 * @name enableSorting
7743 * @propertyOf ui.grid.class:GridOptions
7744 * @description True by default. When enabled, this setting adds sort
7745 * widgets to the column headers, allowing sorting of the data for the entire grid.
7746 * Sorting can then be disabled on individual columns using the columnDefs.
7747 */
7748 baseOptions.enableSorting = baseOptions.enableSorting !== false;
7749
7750 /**
7751 * @ngdoc boolean
7752 * @name enableFiltering
7753 * @propertyOf ui.grid.class:GridOptions
7754 * @description False by default. When enabled, this setting adds filter
7755 * boxes to each column header, allowing filtering within the column for the entire grid.
7756 * Filtering can then be disabled on individual columns using the columnDefs.
7757 */
7758 baseOptions.enableFiltering = baseOptions.enableFiltering === true;
7759
7760 /**
7761 * @ngdoc boolean
7762 * @name enableColumnMenus
7763 * @propertyOf ui.grid.class:GridOptions
7764 * @description True by default. When enabled, this setting displays a column
7765 * menu within each column.
7766 */
7767 baseOptions.enableColumnMenus = baseOptions.enableColumnMenus !== false;
7768
7769 /**
7770 * @ngdoc boolean
7771 * @name enableVerticalScrollbar
7772 * @propertyOf ui.grid.class:GridOptions
7773 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the vertical scrollbar for the grid.
7774 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7775 */
7776 baseOptions.enableVerticalScrollbar = typeof(baseOptions.enableVerticalScrollbar) !== "undefined" ? baseOptions.enableVerticalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7777
7778 /**
7779 * @ngdoc boolean
7780 * @name enableHorizontalScrollbar
7781 * @propertyOf ui.grid.class:GridOptions
7782 * @description uiGridConstants.scrollbars.ALWAYS by default. This settings controls the horizontal scrollbar for the grid.
7783 * Supported values: uiGridConstants.scrollbars.ALWAYS, uiGridConstants.scrollbars.NEVER
7784 */
7785 baseOptions.enableHorizontalScrollbar = typeof(baseOptions.enableHorizontalScrollbar) !== "undefined" ? baseOptions.enableHorizontalScrollbar : uiGridConstants.scrollbars.ALWAYS;
7786
7787 /**
7788 * @ngdoc boolean
7789 * @name enableMinHeightCheck
7790 * @propertyOf ui.grid.class:GridOptions
7791 * @description True by default. When enabled, a newly initialized grid will check to see if it is tall enough to display
7792 * at least one row of data. If the grid is not tall enough, it will resize the DOM element to display minRowsToShow number
7793 * of rows.
7794 */
7795 baseOptions.enableMinHeightCheck = baseOptions.enableMinHeightCheck !== false;
7796
7797 /**
7798 * @ngdoc boolean
7799 * @name minimumColumnSize
7800 * @propertyOf ui.grid.class:GridOptions
7801 * @description Columns can't be smaller than this, defaults to 10 pixels
7802 */
7803 baseOptions.minimumColumnSize = typeof(baseOptions.minimumColumnSize) !== "undefined" ? baseOptions.minimumColumnSize : 10;
7804
7805 /**
7806 * @ngdoc function
7807 * @name rowEquality
7808 * @methodOf ui.grid.class:GridOptions
7809 * @description By default, rows are compared using object equality. This option can be overridden
7810 * to compare on any data item property or function
7811 * @param {object} entityA First Data Item to compare
7812 * @param {object} entityB Second Data Item to compare
7813 */
7814 baseOptions.rowEquality = baseOptions.rowEquality || function(entityA, entityB) {
7815 return entityA === entityB;
7816 };
7817
7818 /**
7819 * @ngdoc string
7820 * @name headerTemplate
7821 * @propertyOf ui.grid.class:GridOptions
7822 * @description Null by default. When provided, this setting uses a custom header
7823 * template, rather than the default template. Can be set to either the name of a template file:
7824 * <pre> $scope.gridOptions.headerTemplate = 'header_template.html';</pre>
7825 * inline html
7826 * <pre> $scope.gridOptions.headerTemplate = '<div class="ui-grid-top-panel" style="text-align: center">I am a Custom Grid Header</div>'</pre>
7827 * or the id of a precompiled template (TBD how to use this).
7828 * </br>Refer to the custom header tutorial for more information.
7829 * If you want no header at all, you can set to an empty div:
7830 * <pre> $scope.gridOptions.headerTemplate = '<div></div>';</pre>
7831 *
7832 * If you want to only have a static header, then you can set to static content. If
7833 * you want to tailor the existing column headers, then you should look at the
7834 * current 'ui-grid-header.html' template in github as your starting point.
7835 *
7836 */
7837 baseOptions.headerTemplate = baseOptions.headerTemplate || null;
7838
7839 /**
7840 * @ngdoc string
7841 * @name footerTemplate
7842 * @propertyOf ui.grid.class:GridOptions
7843 * @description (optional) ui-grid/ui-grid-footer by default. This footer shows the per-column
7844 * aggregation totals.
7845 * When provided, this setting uses a custom footer template. Can be set to either the name of a template file 'footer_template.html', inline html
7846 * <pre>'<div class="ui-grid-bottom-panel" style="text-align: center">I am a Custom Grid Footer</div>'</pre>, or the id
7847 * of a precompiled template (TBD how to use this). Refer to the custom footer tutorial for more information.
7848 */
7849 baseOptions.footerTemplate = baseOptions.footerTemplate || 'ui-grid/ui-grid-footer';
7850
7851 /**
7852 * @ngdoc string
7853 * @name gridFooterTemplate
7854 * @propertyOf ui.grid.class:GridOptions
7855 * @description (optional) ui-grid/ui-grid-grid-footer by default. This template by default shows the
7856 * total items at the bottom of the grid, and the selected items if selection is enabled.
7857 */
7858 baseOptions.gridFooterTemplate = baseOptions.gridFooterTemplate || 'ui-grid/ui-grid-grid-footer';
7859
7860 /**
7861 * @ngdoc string
7862 * @name rowTemplate
7863 * @propertyOf ui.grid.class:GridOptions
7864 * @description 'ui-grid/ui-grid-row' by default. When provided, this setting uses a
7865 * custom row template. Can be set to either the name of a template file:
7866 * <pre> $scope.gridOptions.rowTemplate = 'row_template.html';</pre>
7867 * inline html
7868 * <pre> $scope.gridOptions.rowTemplate = '<div style="background-color: aquamarine" ng-click="grid.appScope.fnOne(row)" ng-repeat="col in colContainer.renderedColumns track by col.colDef.name" class="ui-grid-cell" ui-grid-cell></div>';</pre>
7869 * or the id of a precompiled template (TBD how to use this) can be provided.
7870 * </br>Refer to the custom row template tutorial for more information.
7871 */
7872 baseOptions.rowTemplate = baseOptions.rowTemplate || 'ui-grid/ui-grid-row';
7873
7874 /**
7875 * @ngdoc object
7876 * @name appScopeProvider
7877 * @propertyOf ui.grid.class:GridOptions
7878 * @description by default, the parent scope of the ui-grid element will be assigned to grid.appScope
7879 * this property allows you to assign any reference you want to grid.appScope
7880 */
7881 baseOptions.appScopeProvider = baseOptions.appScopeProvider || null;
7882
7883 return baseOptions;
7884 }
7885 };
7886
7887
7888 }]);
7889
7890 })();
7891
7892 (function(){
7893
7894 angular.module('ui.grid')
7895
7896 /**
7897 * @ngdoc function
7898 * @name ui.grid.class:GridRenderContainer
7899 * @description The grid has render containers, allowing the ability to have pinned columns. If the grid
7900 * is right-to-left then there may be a right render container, if left-to-right then there may
7901 * be a left render container. There is always a body render container.
7902 * @param {string} name The name of the render container ('body', 'left', or 'right')
7903 * @param {Grid} grid the grid the render container is in
7904 * @param {object} options the render container options
7905 */
7906 .factory('GridRenderContainer', ['gridUtil', 'uiGridConstants', function(gridUtil, uiGridConstants) {
7907 function GridRenderContainer(name, grid, options) {
7908 var self = this;
7909
7910 // if (gridUtil.type(grid) !== 'Grid') {
7911 // throw new Error('Grid argument is not a Grid object');
7912 // }
7913
7914 self.name = name;
7915
7916 self.grid = grid;
7917
7918 // self.rowCache = [];
7919 // self.columnCache = [];
7920
7921 self.visibleRowCache = [];
7922 self.visibleColumnCache = [];
7923
7924 self.renderedRows = [];
7925 self.renderedColumns = [];
7926
7927 self.prevScrollTop = 0;
7928 self.prevScrolltopPercentage = 0;
7929 self.prevRowScrollIndex = 0;
7930
7931 self.prevScrollLeft = 0;
7932 self.prevScrollleftPercentage = 0;
7933 self.prevColumnScrollIndex = 0;
7934
7935 self.columnStyles = "";
7936
7937 self.viewportAdjusters = [];
7938
7939 /**
7940 * @ngdoc boolean
7941 * @name hasHScrollbar
7942 * @propertyOf ui.grid.class:GridRenderContainer
7943 * @description flag to signal that container has a horizontal scrollbar
7944 */
7945 self.hasHScrollbar = false;
7946
7947 /**
7948 * @ngdoc boolean
7949 * @name hasVScrollbar
7950 * @propertyOf ui.grid.class:GridRenderContainer
7951 * @description flag to signal that container has a vertical scrollbar
7952 */
7953 self.hasVScrollbar = false;
7954
7955 /**
7956 * @ngdoc boolean
7957 * @name canvasHeightShouldUpdate
7958 * @propertyOf ui.grid.class:GridRenderContainer
7959 * @description flag to signal that container should recalculate the canvas size
7960 */
7961 self.canvasHeightShouldUpdate = true;
7962
7963 /**
7964 * @ngdoc boolean
7965 * @name canvasHeight
7966 * @propertyOf ui.grid.class:GridRenderContainer
7967 * @description last calculated canvas height value
7968 */
7969 self.$$canvasHeight = 0;
7970
7971 if (options && angular.isObject(options)) {
7972 angular.extend(self, options);
7973 }
7974
7975 grid.registerStyleComputation({
7976 priority: 5,
7977 func: function () {
7978 self.updateColumnWidths();
7979 return self.columnStyles;
7980 }
7981 });
7982 }
7983
7984
7985 GridRenderContainer.prototype.reset = function reset() {
7986 // this.rowCache.length = 0;
7987 // this.columnCache.length = 0;
7988
7989 this.visibleColumnCache.length = 0;
7990 this.visibleRowCache.length = 0;
7991
7992 this.renderedRows.length = 0;
7993 this.renderedColumns.length = 0;
7994 };
7995
7996 // TODO(c0bra): calculate size?? Should this be in a stackable directive?
7997
7998
7999 GridRenderContainer.prototype.containsColumn = function (col) {
8000 return this.visibleColumnCache.indexOf(col) !== -1;
8001 };
8002
8003 GridRenderContainer.prototype.minRowsToRender = function minRowsToRender() {
8004 var self = this;
8005 var minRows = 0;
8006 var rowAddedHeight = 0;
8007 var viewPortHeight = self.getViewportHeight();
8008 for (var i = self.visibleRowCache.length - 1; rowAddedHeight < viewPortHeight && i >= 0; i--) {
8009 rowAddedHeight += self.visibleRowCache[i].height;
8010 minRows++;
8011 }
8012 return minRows;
8013 };
8014
8015 GridRenderContainer.prototype.minColumnsToRender = function minColumnsToRender() {
8016 var self = this;
8017 var viewportWidth = this.getViewportWidth();
8018
8019 var min = 0;
8020 var totalWidth = 0;
8021 // self.columns.forEach(function(col, i) {
8022 for (var i = 0; i < self.visibleColumnCache.length; i++) {
8023 var col = self.visibleColumnCache[i];
8024
8025 if (totalWidth < viewportWidth) {
8026 totalWidth += col.drawnWidth ? col.drawnWidth : 0;
8027 min++;
8028 }
8029 else {
8030 var currWidth = 0;
8031 for (var j = i; j >= i - min; j--) {
8032 currWidth += self.visibleColumnCache[j].drawnWidth ? self.visibleColumnCache[j].drawnWidth : 0;
8033 }
8034 if (currWidth < viewportWidth) {
8035 min++;
8036 }
8037 }
8038 }
8039
8040 return min;
8041 };
8042
8043 GridRenderContainer.prototype.getVisibleRowCount = function getVisibleRowCount() {
8044 return this.visibleRowCache.length;
8045 };
8046
8047 /**
8048 * @ngdoc function
8049 * @name registerViewportAdjuster
8050 * @methodOf ui.grid.class:GridRenderContainer
8051 * @description Registers an adjuster to the render container's available width or height. Adjusters are used
8052 * to tell the render container that there is something else consuming space, and to adjust it's size
8053 * appropriately.
8054 * @param {function} func the adjuster function we want to register
8055 */
8056
8057 GridRenderContainer.prototype.registerViewportAdjuster = function registerViewportAdjuster(func) {
8058 this.viewportAdjusters.push(func);
8059 };
8060
8061 /**
8062 * @ngdoc function
8063 * @name removeViewportAdjuster
8064 * @methodOf ui.grid.class:GridRenderContainer
8065 * @description Removes an adjuster, should be used when your element is destroyed
8066 * @param {function} func the adjuster function we want to remove
8067 */
8068 GridRenderContainer.prototype.removeViewportAdjuster = function removeViewportAdjuster(func) {
8069 var idx = this.viewportAdjusters.indexOf(func);
8070
8071 if (idx > -1) {
8072 this.viewportAdjusters.splice(idx, 1);
8073 }
8074 };
8075
8076 /**
8077 * @ngdoc function
8078 * @name getViewportAdjustment
8079 * @methodOf ui.grid.class:GridRenderContainer
8080 * @description Gets the adjustment based on the viewportAdjusters.
8081 * @returns {object} a hash of { height: x, width: y }. Usually the values will be negative
8082 */
8083 GridRenderContainer.prototype.getViewportAdjustment = function getViewportAdjustment() {
8084 var self = this;
8085
8086 var adjustment = { height: 0, width: 0 };
8087
8088 self.viewportAdjusters.forEach(function (func) {
8089 adjustment = func.call(this, adjustment);
8090 });
8091
8092 return adjustment;
8093 };
8094
8095 GridRenderContainer.prototype.getMargin = function getMargin(side) {
8096 var self = this;
8097
8098 var amount = 0;
8099
8100 self.viewportAdjusters.forEach(function (func) {
8101 var adjustment = func.call(this, { height: 0, width: 0 });
8102
8103 if (adjustment.side && adjustment.side === side) {
8104 amount += adjustment.width * -1;
8105 }
8106 });
8107
8108 return amount;
8109 };
8110
8111 GridRenderContainer.prototype.getViewportHeight = function getViewportHeight() {
8112 var self = this;
8113
8114 var headerHeight = (self.headerHeight) ? self.headerHeight : self.grid.headerHeight;
8115
8116 var viewPortHeight = self.grid.gridHeight - headerHeight - self.grid.footerHeight;
8117
8118
8119 var adjustment = self.getViewportAdjustment();
8120
8121 viewPortHeight = viewPortHeight + adjustment.height;
8122
8123 return viewPortHeight;
8124 };
8125
8126 GridRenderContainer.prototype.getViewportWidth = function getViewportWidth() {
8127 var self = this;
8128
8129 var viewportWidth = self.grid.gridWidth;
8130
8131 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8132 // viewPortWidth = viewPortWidth - self.grid.verticalScrollbarWidth;
8133 //}
8134
8135 // var viewportWidth = 0;\
8136 // self.visibleColumnCache.forEach(function (column) {
8137 // viewportWidth += column.drawnWidth;
8138 // });
8139
8140 var adjustment = self.getViewportAdjustment();
8141
8142 viewportWidth = viewportWidth + adjustment.width;
8143
8144 return viewportWidth;
8145 };
8146
8147 GridRenderContainer.prototype.getHeaderViewportWidth = function getHeaderViewportWidth() {
8148 var self = this;
8149
8150 var viewportWidth = this.getViewportWidth();
8151
8152 //if (typeof(self.grid.verticalScrollbarWidth) !== 'undefined' && self.grid.verticalScrollbarWidth !== undefined && self.grid.verticalScrollbarWidth > 0) {
8153 // viewPortWidth = viewPortWidth + self.grid.verticalScrollbarWidth;
8154 //}
8155
8156 // var adjustment = self.getViewportAdjustment();
8157 // viewPortWidth = viewPortWidth + adjustment.width;
8158
8159 return viewportWidth;
8160 };
8161
8162
8163 /**
8164 * @ngdoc function
8165 * @name getCanvasHeight
8166 * @methodOf ui.grid.class:GridRenderContainer
8167 * @description Returns the total canvas height. Only recalculates if canvasHeightShouldUpdate = false
8168 * @returns {number} total height of all the visible rows in the container
8169 */
8170 GridRenderContainer.prototype.getCanvasHeight = function getCanvasHeight() {
8171 var self = this;
8172
8173 if (!self.canvasHeightShouldUpdate) {
8174 return self.$$canvasHeight;
8175 }
8176
8177 var oldCanvasHeight = self.$$canvasHeight;
8178
8179 self.$$canvasHeight = 0;
8180
8181 self.visibleRowCache.forEach(function(row){
8182 self.$$canvasHeight += row.height;
8183 });
8184
8185
8186 self.canvasHeightShouldUpdate = false;
8187
8188 self.grid.api.core.raise.canvasHeightChanged(oldCanvasHeight, self.$$canvasHeight);
8189
8190 return self.$$canvasHeight;
8191 };
8192
8193 GridRenderContainer.prototype.getVerticalScrollLength = function getVerticalScrollLength() {
8194 return this.getCanvasHeight() - this.getViewportHeight() + this.grid.scrollbarHeight;
8195 };
8196
8197 GridRenderContainer.prototype.getCanvasWidth = function getCanvasWidth() {
8198 var self = this;
8199
8200 var ret = self.canvasWidth;
8201
8202 return ret;
8203 };
8204
8205 GridRenderContainer.prototype.setRenderedRows = function setRenderedRows(newRows) {
8206 this.renderedRows.length = newRows.length;
8207 for (var i = 0; i < newRows.length; i++) {
8208 this.renderedRows[i] = newRows[i];
8209 }
8210 };
8211
8212 GridRenderContainer.prototype.setRenderedColumns = function setRenderedColumns(newColumns) {
8213 var self = this;
8214
8215 // OLD:
8216 this.renderedColumns.length = newColumns.length;
8217 for (var i = 0; i < newColumns.length; i++) {
8218 this.renderedColumns[i] = newColumns[i];
8219 }
8220
8221 this.updateColumnOffset();
8222 };
8223
8224 GridRenderContainer.prototype.updateColumnOffset = function updateColumnOffset() {
8225 // Calculate the width of the columns on the left side that are no longer rendered.
8226 // That will be the offset for the columns as we scroll horizontally.
8227 var hiddenColumnsWidth = 0;
8228 for (var i = 0; i < this.currentFirstColumn; i++) {
8229 hiddenColumnsWidth += this.visibleColumnCache[i].drawnWidth;
8230 }
8231
8232 this.columnOffset = hiddenColumnsWidth;
8233 };
8234
8235 GridRenderContainer.prototype.scrollVertical = function (newScrollTop) {
8236 var vertScrollPercentage = -1;
8237
8238 if (newScrollTop !== this.prevScrollTop) {
8239 var yDiff = newScrollTop - this.prevScrollTop;
8240
8241 if (yDiff > 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; }
8242 if (yDiff < 0 ) { this.grid.scrollDirection = uiGridConstants.scrollDirection.UP; }
8243
8244 var vertScrollLength = this.getVerticalScrollLength();
8245
8246 vertScrollPercentage = newScrollTop / vertScrollLength;
8247
8248 // console.log('vert', vertScrollPercentage, newScrollTop, vertScrollLength);
8249
8250 if (vertScrollPercentage > 1) { vertScrollPercentage = 1; }
8251 if (vertScrollPercentage < 0) { vertScrollPercentage = 0; }
8252
8253 this.adjustScrollVertical(newScrollTop, vertScrollPercentage);
8254 return vertScrollPercentage;
8255 }
8256 };
8257
8258 GridRenderContainer.prototype.scrollHorizontal = function(newScrollLeft){
8259 var horizScrollPercentage = -1;
8260
8261 // Handle RTL here
8262
8263 if (newScrollLeft !== this.prevScrollLeft) {
8264 var xDiff = newScrollLeft - this.prevScrollLeft;
8265
8266 if (xDiff > 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; }
8267 if (xDiff < 0) { this.grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; }
8268
8269 var horizScrollLength = (this.canvasWidth - this.getViewportWidth());
8270 if (horizScrollLength !== 0) {
8271 horizScrollPercentage = newScrollLeft / horizScrollLength;
8272 }
8273 else {
8274 horizScrollPercentage = 0;
8275 }
8276
8277 this.adjustScrollHorizontal(newScrollLeft, horizScrollPercentage);
8278 return horizScrollPercentage;
8279 }
8280 };
8281
8282 GridRenderContainer.prototype.adjustScrollVertical = function adjustScrollVertical(scrollTop, scrollPercentage, force) {
8283 if (this.prevScrollTop === scrollTop && !force) {
8284 return;
8285 }
8286
8287 if (typeof(scrollTop) === 'undefined' || scrollTop === undefined || scrollTop === null) {
8288 scrollTop = (this.getCanvasHeight() - this.getViewportHeight()) * scrollPercentage;
8289 }
8290
8291 this.adjustRows(scrollTop, scrollPercentage, false);
8292
8293 this.prevScrollTop = scrollTop;
8294 this.prevScrolltopPercentage = scrollPercentage;
8295
8296 this.grid.queueRefresh();
8297 };
8298
8299 GridRenderContainer.prototype.adjustScrollHorizontal = function adjustScrollHorizontal(scrollLeft, scrollPercentage, force) {
8300 if (this.prevScrollLeft === scrollLeft && !force) {
8301 return;
8302 }
8303
8304 if (typeof(scrollLeft) === 'undefined' || scrollLeft === undefined || scrollLeft === null) {
8305 scrollLeft = (this.getCanvasWidth() - this.getViewportWidth()) * scrollPercentage;
8306 }
8307
8308 this.adjustColumns(scrollLeft, scrollPercentage);
8309
8310 this.prevScrollLeft = scrollLeft;
8311 this.prevScrollleftPercentage = scrollPercentage;
8312
8313 this.grid.queueRefresh();
8314 };
8315
8316 GridRenderContainer.prototype.adjustRows = function adjustRows(scrollTop, scrollPercentage, postDataLoaded) {
8317 var self = this;
8318
8319 var minRows = self.minRowsToRender();
8320
8321 var rowCache = self.visibleRowCache;
8322
8323 var maxRowIndex = rowCache.length - minRows;
8324
8325 // console.log('scroll%1', scrollPercentage);
8326
8327 // Calculate the scroll percentage according to the scrollTop location, if no percentage was provided
8328 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollTop) {
8329 scrollPercentage = scrollTop / self.getVerticalScrollLength();
8330 }
8331
8332 var rowIndex = Math.ceil(Math.min(maxRowIndex, maxRowIndex * scrollPercentage));
8333
8334 // console.log('maxRowIndex / scroll%', maxRowIndex, scrollPercentage, rowIndex);
8335
8336 // Define a max row index that we can't scroll past
8337 if (rowIndex > maxRowIndex) {
8338 rowIndex = maxRowIndex;
8339 }
8340
8341 var newRange = [];
8342 if (rowCache.length > self.grid.options.virtualizationThreshold) {
8343 if (!(typeof(scrollTop) === 'undefined' || scrollTop === null)) {
8344 // Have we hit the threshold going down?
8345 if ( !self.grid.suppressParentScrollDown && self.prevScrollTop < scrollTop && rowIndex < self.prevRowScrollIndex + self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8346 return;
8347 }
8348 //Have we hit the threshold going up?
8349 if ( !self.grid.suppressParentScrollUp && self.prevScrollTop > scrollTop && rowIndex > self.prevRowScrollIndex - self.grid.options.scrollThreshold && rowIndex < maxRowIndex) {
8350 return;
8351 }
8352 }
8353 var rangeStart = {};
8354 var rangeEnd = {};
8355
8356 rangeStart = Math.max(0, rowIndex - self.grid.options.excessRows);
8357 rangeEnd = Math.min(rowCache.length, rowIndex + minRows + self.grid.options.excessRows);
8358
8359 newRange = [rangeStart, rangeEnd];
8360 }
8361 else {
8362 var maxLen = self.visibleRowCache.length;
8363 newRange = [0, Math.max(maxLen, minRows + self.grid.options.excessRows)];
8364 }
8365
8366 self.updateViewableRowRange(newRange);
8367
8368 self.prevRowScrollIndex = rowIndex;
8369 };
8370
8371 GridRenderContainer.prototype.adjustColumns = function adjustColumns(scrollLeft, scrollPercentage) {
8372 var self = this;
8373
8374 var minCols = self.minColumnsToRender();
8375
8376 var columnCache = self.visibleColumnCache;
8377 var maxColumnIndex = columnCache.length - minCols;
8378
8379 // Calculate the scroll percentage according to the scrollLeft location, if no percentage was provided
8380 if ((typeof(scrollPercentage) === 'undefined' || scrollPercentage === null) && scrollLeft) {
8381 var horizScrollLength = (self.getCanvasWidth() - self.getViewportWidth());
8382 scrollPercentage = scrollLeft / horizScrollLength;
8383 }
8384
8385 var colIndex = Math.ceil(Math.min(maxColumnIndex, maxColumnIndex * scrollPercentage));
8386
8387 // Define a max row index that we can't scroll past
8388 if (colIndex > maxColumnIndex) {
8389 colIndex = maxColumnIndex;
8390 }
8391
8392 var newRange = [];
8393 if (columnCache.length > self.grid.options.columnVirtualizationThreshold && self.getCanvasWidth() > self.getViewportWidth()) {
8394 /* Commented the following lines because otherwise the moved column wasn't visible immediately on the new position
8395 * in the case of many columns with horizontal scroll, one had to scroll left or right and then return in order to see it
8396 // Have we hit the threshold going down?
8397 if (self.prevScrollLeft < scrollLeft && colIndex < self.prevColumnScrollIndex + self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8398 return;
8399 }
8400 //Have we hit the threshold going up?
8401 if (self.prevScrollLeft > scrollLeft && colIndex > self.prevColumnScrollIndex - self.grid.options.horizontalScrollThreshold && colIndex < maxColumnIndex) {
8402 return;
8403 }*/
8404
8405 var rangeStart = Math.max(0, colIndex - self.grid.options.excessColumns);
8406 var rangeEnd = Math.min(columnCache.length, colIndex + minCols + self.grid.options.excessColumns);
8407
8408 newRange = [rangeStart, rangeEnd];
8409 }
8410 else {
8411 var maxLen = self.visibleColumnCache.length;
8412
8413 newRange = [0, Math.max(maxLen, minCols + self.grid.options.excessColumns)];
8414 }
8415
8416 self.updateViewableColumnRange(newRange);
8417
8418 self.prevColumnScrollIndex = colIndex;
8419 };
8420
8421 // Method for updating the visible rows
8422 GridRenderContainer.prototype.updateViewableRowRange = function updateViewableRowRange(renderedRange) {
8423 // Slice out the range of rows from the data
8424 // var rowArr = uiGridCtrl.grid.rows.slice(renderedRange[0], renderedRange[1]);
8425 var rowArr = this.visibleRowCache.slice(renderedRange[0], renderedRange[1]);
8426
8427 // Define the top-most rendered row
8428 this.currentTopRow = renderedRange[0];
8429
8430 this.setRenderedRows(rowArr);
8431 };
8432
8433 // Method for updating the visible columns
8434 GridRenderContainer.prototype.updateViewableColumnRange = function updateViewableColumnRange(renderedRange) {
8435 // Slice out the range of rows from the data
8436 // var columnArr = uiGridCtrl.grid.columns.slice(renderedRange[0], renderedRange[1]);
8437 var columnArr = this.visibleColumnCache.slice(renderedRange[0], renderedRange[1]);
8438
8439 // Define the left-most rendered columns
8440 this.currentFirstColumn = renderedRange[0];
8441
8442 this.setRenderedColumns(columnArr);
8443 };
8444
8445 GridRenderContainer.prototype.headerCellWrapperStyle = function () {
8446 var self = this;
8447
8448 if (self.currentFirstColumn !== 0) {
8449 var offset = self.columnOffset;
8450
8451 if (self.grid.isRTL()) {
8452 return { 'margin-right': offset + 'px' };
8453 }
8454 else {
8455 return { 'margin-left': offset + 'px' };
8456 }
8457 }
8458
8459 return null;
8460 };
8461
8462 /**
8463 * @ngdoc boolean
8464 * @name updateColumnWidths
8465 * @propertyOf ui.grid.class:GridRenderContainer
8466 * @description Determine the appropriate column width of each column across all render containers.
8467 *
8468 * Column width is easy when each column has a specified width. When columns are variable width (i.e.
8469 * have an * or % of the viewport) then we try to calculate so that things fit in. The problem is that
8470 * we have multiple render containers, and we don't want one render container to just take the whole viewport
8471 * when it doesn't need to - we want things to balance out across the render containers.
8472 *
8473 * To do this, we use this method to calculate all the renderContainers, recognising that in a given render
8474 * cycle it'll get called once per render container, so it needs to return the same values each time.
8475 *
8476 * The constraints on this method are therefore:
8477 * - must return the same value when called multiple times, to do this it needs to rely on properties of the
8478 * columns, but not properties that change when this is called (so it shouldn't rely on drawnWidth)
8479 *
8480 * The general logic of this method is:
8481 * - calculate our total available width
8482 * - look at all the columns across all render containers, and work out which have widths and which have
8483 * constraints such as % or * or something else
8484 * - for those with *, count the total number of * we see and add it onto a running total, add this column to an * array
8485 * - for those with a %, allocate the % as a percentage of the viewport, having consideration of min and max
8486 * - for those with manual width (in pixels) we set the drawnWidth to the specified width
8487 * - we end up with an asterisks array still to process
8488 * - we look at our remaining width. If it's greater than zero, we divide it up among the asterisk columns, then process
8489 * them for min and max width constraints
8490 * - if it's zero or less, we set the asterisk columns to their minimum widths
8491 * - we use parseInt quite a bit, as we try to make all our column widths integers
8492 */
8493 GridRenderContainer.prototype.updateColumnWidths = function () {
8494 var self = this;
8495
8496 var asterisksArray = [],
8497 asteriskNum = 0,
8498 usedWidthSum = 0,
8499 ret = '';
8500
8501 // Get the width of the viewport
8502 var availableWidth = self.grid.getViewportWidth() - self.grid.scrollbarWidth;
8503
8504 // get all the columns across all render containers, we have to calculate them all or one render container
8505 // could consume the whole viewport
8506 var columnCache = [];
8507 angular.forEach(self.grid.renderContainers, function( container, name){
8508 columnCache = columnCache.concat(container.visibleColumnCache);
8509 });
8510
8511 // look at each column, process any manual values or %, put the * into an array to look at later
8512 columnCache.forEach(function(column, i) {
8513 var width = 0;
8514 // Skip hidden columns
8515 if (!column.visible) { return; }
8516
8517 if (angular.isNumber(column.width)) {
8518 // pixel width, set to this value
8519 width = parseInt(column.width, 10);
8520 usedWidthSum = usedWidthSum + width;
8521 column.drawnWidth = width;
8522
8523 } else if (gridUtil.endsWith(column.width, "%")) {
8524 // percentage width, set to percentage of the viewport
8525 width = parseInt(parseInt(column.width.replace(/%/g, ''), 10) / 100 * availableWidth);
8526
8527 if ( width > column.maxWidth ){
8528 width = column.maxWidth;
8529 }
8530
8531 if ( width < column.minWidth ){
8532 width = column.minWidth;
8533 }
8534
8535 usedWidthSum = usedWidthSum + width;
8536 column.drawnWidth = width;
8537 } else if (angular.isString(column.width) && column.width.indexOf('*') !== -1) {
8538 // is an asterisk column, the gridColumn already checked the string consists only of '****'
8539 asteriskNum = asteriskNum + column.width.length;
8540 asterisksArray.push(column);
8541 }
8542 });
8543
8544 // Get the remaining width (available width subtracted by the used widths sum)
8545 var remainingWidth = availableWidth - usedWidthSum;
8546
8547 var i, column, colWidth;
8548
8549 if (asterisksArray.length > 0) {
8550 // the width that each asterisk value would be assigned (this can be negative)
8551 var asteriskVal = remainingWidth / asteriskNum;
8552
8553 asterisksArray.forEach(function( column ){
8554 var width = parseInt(column.width.length * asteriskVal, 10);
8555
8556 if ( width > column.maxWidth ){
8557 width = column.maxWidth;
8558 }
8559
8560 if ( width < column.minWidth ){
8561 width = column.minWidth;
8562 }
8563
8564 usedWidthSum = usedWidthSum + width;
8565 column.drawnWidth = width;
8566 });
8567 }
8568
8569 // If the grid width didn't divide evenly into the column widths and we have pixels left over, or our
8570 // calculated widths would have the grid narrower than the available space,
8571 // dole the remainder out one by one to make everything fit
8572 var processColumnUpwards = function(column){
8573 if ( column.drawnWidth < column.maxWidth && leftoverWidth > 0) {
8574 column.drawnWidth++;
8575 usedWidthSum++;
8576 leftoverWidth--;
8577 columnsToChange = true;
8578 }
8579 };
8580
8581 var leftoverWidth = availableWidth - usedWidthSum;
8582 var columnsToChange = true;
8583
8584 while (leftoverWidth > 0 && columnsToChange) {
8585 columnsToChange = false;
8586 asterisksArray.forEach(processColumnUpwards);
8587 }
8588
8589 // We can end up with too much width even though some columns aren't at their max width, in this situation
8590 // we can trim the columns a little
8591 var processColumnDownwards = function(column){
8592 if ( column.drawnWidth > column.minWidth && excessWidth > 0) {
8593 column.drawnWidth--;
8594 usedWidthSum--;
8595 excessWidth--;
8596 columnsToChange = true;
8597 }
8598 };
8599
8600 var excessWidth = usedWidthSum - availableWidth;
8601 columnsToChange = true;
8602
8603 while (excessWidth > 0 && columnsToChange) {
8604 columnsToChange = false;
8605 asterisksArray.forEach(processColumnDownwards);
8606 }
8607
8608
8609 // all that was across all the renderContainers, now we need to work out what that calculation decided for
8610 // our renderContainer
8611 var canvasWidth = 0;
8612 self.visibleColumnCache.forEach(function(column){
8613 if ( column.visible ){
8614 canvasWidth = canvasWidth + column.drawnWidth;
8615 }
8616 });
8617
8618 // Build the CSS
8619 columnCache.forEach(function (column) {
8620 ret = ret + column.getColClassDefinition();
8621 });
8622
8623 self.canvasWidth = canvasWidth;
8624
8625 // Return the styles back to buildStyles which pops them into the `customStyles` scope variable
8626 // return ret;
8627
8628 // Set this render container's column styles so they can be used in style computation
8629 this.columnStyles = ret;
8630 };
8631
8632 GridRenderContainer.prototype.needsHScrollbarPlaceholder = function () {
8633 return this.grid.options.enableHorizontalScrollbar && !this.hasHScrollbar && !this.grid.disableScrolling;
8634 };
8635
8636 GridRenderContainer.prototype.getViewportStyle = function () {
8637 var self = this;
8638 var styles = {};
8639
8640 self.hasHScrollbar = false;
8641 self.hasVScrollbar = false;
8642
8643 if (self.grid.disableScrolling) {
8644 styles['overflow-x'] = 'hidden';
8645 styles['overflow-y'] = 'hidden';
8646 return styles;
8647 }
8648
8649 if (self.name === 'body') {
8650 self.hasHScrollbar = self.grid.options.enableHorizontalScrollbar !== uiGridConstants.scrollbars.NEVER;
8651 if (!self.grid.isRTL()) {
8652 if (!self.grid.hasRightContainerColumns()) {
8653 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8654 }
8655 }
8656 else {
8657 if (!self.grid.hasLeftContainerColumns()) {
8658 self.hasVScrollbar = self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER;
8659 }
8660 }
8661 }
8662 else if (self.name === 'left') {
8663 self.hasVScrollbar = self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8664 }
8665 else {
8666 self.hasVScrollbar = !self.grid.isRTL() ? self.grid.options.enableVerticalScrollbar !== uiGridConstants.scrollbars.NEVER : false;
8667 }
8668
8669 styles['overflow-x'] = self.hasHScrollbar ? 'scroll' : 'hidden';
8670 styles['overflow-y'] = self.hasVScrollbar ? 'scroll' : 'hidden';
8671
8672
8673 return styles;
8674
8675
8676 };
8677
8678 return GridRenderContainer;
8679 }]);
8680
8681 })();
8682
8683 (function(){
8684
8685 angular.module('ui.grid')
8686 .factory('GridRow', ['gridUtil', function(gridUtil) {
8687
8688 /**
8689 * @ngdoc function
8690 * @name ui.grid.class:GridRow
8691 * @description GridRow is the viewModel for one logical row on the grid. A grid Row is not necessarily a one-to-one
8692 * relation to gridOptions.data.
8693 * @param {object} entity the array item from GridOptions.data
8694 * @param {number} index the current position of the row in the array
8695 * @param {Grid} reference to the parent grid
8696 */
8697 function GridRow(entity, index, grid) {
8698
8699 /**
8700 * @ngdoc object
8701 * @name grid
8702 * @propertyOf ui.grid.class:GridRow
8703 * @description A reference back to the grid
8704 */
8705 this.grid = grid;
8706
8707 /**
8708 * @ngdoc object
8709 * @name entity
8710 * @propertyOf ui.grid.class:GridRow
8711 * @description A reference to an item in gridOptions.data[]
8712 */
8713 this.entity = entity;
8714
8715 /**
8716 * @ngdoc object
8717 * @name uid
8718 * @propertyOf ui.grid.class:GridRow
8719 * @description UniqueId of row
8720 */
8721 this.uid = gridUtil.nextUid();
8722
8723 /**
8724 * @ngdoc object
8725 * @name visible
8726 * @propertyOf ui.grid.class:GridRow
8727 * @description If true, the row will be rendered
8728 */
8729 // Default to true
8730 this.visible = true;
8731
8732
8733 this.$$height = grid.options.rowHeight;
8734
8735 }
8736
8737 /**
8738 * @ngdoc object
8739 * @name height
8740 * @propertyOf ui.grid.class:GridRow
8741 * @description height of each individual row. changing the height will flag all
8742 * row renderContainers to recalculate their canvas height
8743 */
8744 Object.defineProperty(GridRow.prototype, 'height', {
8745 get: function() {
8746 return this.$$height;
8747 },
8748 set: function(height) {
8749 if (height !== this.$$height) {
8750 this.grid.updateCanvasHeight();
8751 this.$$height = height;
8752 }
8753 }
8754 });
8755
8756 /**
8757 * @ngdoc function
8758 * @name getQualifiedColField
8759 * @methodOf ui.grid.class:GridRow
8760 * @description returns the qualified field name as it exists on scope
8761 * ie: row.entity.fieldA
8762 * @param {GridCol} col column instance
8763 * @returns {string} resulting name that can be evaluated on scope
8764 */
8765 GridRow.prototype.getQualifiedColField = function(col) {
8766 return 'row.' + this.getEntityQualifiedColField(col);
8767 };
8768
8769 /**
8770 * @ngdoc function
8771 * @name getEntityQualifiedColField
8772 * @methodOf ui.grid.class:GridRow
8773 * @description returns the qualified field name minus the row path
8774 * ie: entity.fieldA
8775 * @param {GridCol} col column instance
8776 * @returns {string} resulting name that can be evaluated against a row
8777 */
8778 GridRow.prototype.getEntityQualifiedColField = function(col) {
8779 return gridUtil.preEval('entity.' + col.field);
8780 };
8781
8782
8783 /**
8784 * @ngdoc function
8785 * @name setRowInvisible
8786 * @methodOf ui.grid.class:GridRow
8787 * @description Sets an override on the row that forces it to always
8788 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8789 *
8790 * This method can be called from the api, passing in the gridRow we want
8791 * altered. It should really work by calling gridRow.setRowInvisible, but that's
8792 * not the way I coded it, and too late to change now. Changed to just call
8793 * the internal function row.setThisRowInvisible().
8794 *
8795 * @param {GridRow} row the row we want to set to invisible
8796 *
8797 */
8798 GridRow.prototype.setRowInvisible = function ( row ) {
8799 if (row && row.setThisRowInvisible){
8800 row.setThisRowInvisible( 'user' );
8801 }
8802 };
8803
8804
8805 /**
8806 * @ngdoc function
8807 * @name clearRowInvisible
8808 * @methodOf ui.grid.class:GridRow
8809 * @description Clears an override on the row that forces it to always
8810 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility.
8811 *
8812 * This method can be called from the api, passing in the gridRow we want
8813 * altered. It should really work by calling gridRow.clearRowInvisible, but that's
8814 * not the way I coded it, and too late to change now. Changed to just call
8815 * the internal function row.clearThisRowInvisible().
8816 *
8817 * @param {GridRow} row the row we want to clear the invisible flag
8818 *
8819 */
8820 GridRow.prototype.clearRowInvisible = function ( row ) {
8821 if (row && row.clearThisRowInvisible){
8822 row.clearThisRowInvisible( 'user' );
8823 }
8824 };
8825
8826
8827 /**
8828 * @ngdoc function
8829 * @name setThisRowInvisible
8830 * @methodOf ui.grid.class:GridRow
8831 * @description Sets an override on the row that forces it to always
8832 * be invisible. Emits the rowsVisibleChanged event if it changed the row visibility
8833 *
8834 * @param {string} reason the reason (usually the module) for the row to be invisible.
8835 * E.g. grouping, user, filter
8836 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8837 */
8838 GridRow.prototype.setThisRowInvisible = function ( reason, fromRowsProcessor ) {
8839 if ( !this.invisibleReason ){
8840 this.invisibleReason = {};
8841 }
8842 this.invisibleReason[reason] = true;
8843 this.evaluateRowVisibility( fromRowsProcessor);
8844 };
8845
8846
8847 /**
8848 * @ngdoc function
8849 * @name clearRowInvisible
8850 * @methodOf ui.grid.class:GridRow
8851 * @description Clears any override on the row visibility, returning it
8852 * to normal visibility calculations. Emits the rowsVisibleChanged
8853 * event
8854 *
8855 * @param {string} reason the reason (usually the module) for the row to be invisible.
8856 * E.g. grouping, user, filter
8857 * @param {boolean} fromRowsProcessor whether we were called from a rowsProcessor, passed through to evaluateRowVisibility
8858 */
8859 GridRow.prototype.clearThisRowInvisible = function ( reason, fromRowsProcessor ) {
8860 if (typeof(this.invisibleReason) !== 'undefined' ) {
8861 delete this.invisibleReason[reason];
8862 }
8863 this.evaluateRowVisibility( fromRowsProcessor );
8864 };
8865
8866
8867 /**
8868 * @ngdoc function
8869 * @name evaluateRowVisibility
8870 * @methodOf ui.grid.class:GridRow
8871 * @description Determines whether the row should be visible based on invisibleReason,
8872 * and if it changes the row visibility, then emits the rowsVisibleChanged event.
8873 *
8874 * Queues a grid refresh, but doesn't call it directly to avoid hitting lots of grid refreshes.
8875 * @param {boolean} fromRowProcessor if true, then it won't raise events or queue the refresh, the
8876 * row processor does that already
8877 */
8878 GridRow.prototype.evaluateRowVisibility = function ( fromRowProcessor ) {
8879 var newVisibility = true;
8880 if ( typeof(this.invisibleReason) !== 'undefined' ){
8881 angular.forEach(this.invisibleReason, function( value, key ){
8882 if ( value ){
8883 newVisibility = false;
8884 }
8885 });
8886 }
8887
8888 if ( typeof(this.visible) === 'undefined' || this.visible !== newVisibility ){
8889 this.visible = newVisibility;
8890 if ( !fromRowProcessor ){
8891 this.grid.queueGridRefresh();
8892 this.grid.api.core.raise.rowsVisibleChanged(this);
8893 }
8894 }
8895 };
8896
8897
8898 return GridRow;
8899 }]);
8900
8901 })();
8902
8903 (function(){
8904 'use strict';
8905 /**
8906 * @ngdoc object
8907 * @name ui.grid.class:GridRowColumn
8908 * @param {GridRow} row The row for this pair
8909 * @param {GridColumn} column The column for this pair
8910 * @description A row and column pair that represents the intersection of these two entities.
8911 * Must be instantiated as a constructor using the `new` keyword.
8912 */
8913 angular.module('ui.grid')
8914 .factory('GridRowColumn', ['$parse', '$filter',
8915 function GridRowColumnFactory($parse, $filter){
8916 var GridRowColumn = function GridRowColumn(row, col) {
8917 if ( !(this instanceof GridRowColumn)){
8918 throw "Using GridRowColumn as a function insead of as a constructor. Must be called with `new` keyword";
8919 }
8920
8921 /**
8922 * @ngdoc object
8923 * @name row
8924 * @propertyOf ui.grid.class:GridRowColumn
8925 * @description {@link ui.grid.class:GridRow }
8926 */
8927 this.row = row;
8928 /**
8929 * @ngdoc object
8930 * @name col
8931 * @propertyOf ui.grid.class:GridRowColumn
8932 * @description {@link ui.grid.class:GridColumn }
8933 */
8934 this.col = col;
8935 };
8936
8937 /**
8938 * @ngdoc function
8939 * @name getIntersectionValueRaw
8940 * @methodOf ui.grid.class:GridRowColumn
8941 * @description Gets the intersection of where the row and column meet.
8942 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8943 * If the column has a cellFilter this will NOT return the filtered value.
8944 */
8945 GridRowColumn.prototype.getIntersectionValueRaw = function(){
8946 var getter = $parse(this.row.getEntityQualifiedColField(this.col));
8947 var context = this.row;
8948 return getter(context);
8949 };
8950 /**
8951 * @ngdoc function
8952 * @name getIntersectionValueFiltered
8953 * @methodOf ui.grid.class:GridRowColumn
8954 * @description Gets the intersection of where the row and column meet.
8955 * @returns {String|Number|Object} The value from the grid data that this GridRowColumn points too.
8956 * If the column has a cellFilter this will also apply the filter to it and return the value that the filter displays.
8957 */
8958 GridRowColumn.prototype.getIntersectionValueFiltered = function(){
8959 var value = this.getIntersectionValueRaw();
8960 if (this.col.cellFilter && this.col.cellFilter !== ''){
8961 var getFilterIfExists = function(filterName){
8962 try {
8963 return $filter(filterName);
8964 } catch (e){
8965 return null;
8966 }
8967 };
8968 var filter = getFilterIfExists(this.col.cellFilter);
8969 if (filter) { // Check if this is filter name or a filter string
8970 value = filter(value);
8971 } else { // We have the template version of a filter so we need to parse it apart
8972 // Get the filter params out using a regex
8973 // Test out this regex here https://regex101.com/r/rC5eR5/2
8974 var re = /([^:]*):([^:]*):?([\s\S]+)?/;
8975 var matches;
8976 if ((matches = re.exec(this.col.cellFilter)) !== null) {
8977 // View your result using the matches-variable.
8978 // eg matches[0] etc.
8979 value = $filter(matches[1])(value, matches[2], matches[3]);
8980 }
8981 }
8982 }
8983 return value;
8984 };
8985 return GridRowColumn;
8986 }
8987 ]);
8988 })();
8989
8990 (function () {
8991 angular.module('ui.grid')
8992 .factory('ScrollEvent', ['gridUtil', function (gridUtil) {
8993
8994 /**
8995 * @ngdoc function
8996 * @name ui.grid.class:ScrollEvent
8997 * @description Model for all scrollEvents
8998 * @param {Grid} grid that owns the scroll event
8999 * @param {GridRenderContainer} sourceRowContainer that owns the scroll event. Can be null
9000 * @param {GridRenderContainer} sourceColContainer that owns the scroll event. Can be null
9001 * @param {string} source the source of the event - from uiGridConstants.scrollEventSources or a string value of directive/service/factory.functionName
9002 */
9003 function ScrollEvent(grid, sourceRowContainer, sourceColContainer, source) {
9004 var self = this;
9005 if (!grid) {
9006 throw new Error("grid argument is required");
9007 }
9008
9009 /**
9010 * @ngdoc object
9011 * @name grid
9012 * @propertyOf ui.grid.class:ScrollEvent
9013 * @description A reference back to the grid
9014 */
9015 self.grid = grid;
9016
9017
9018
9019 /**
9020 * @ngdoc object
9021 * @name source
9022 * @propertyOf ui.grid.class:ScrollEvent
9023 * @description the source of the scroll event. limited to values from uiGridConstants.scrollEventSources
9024 */
9025 self.source = source;
9026
9027
9028 /**
9029 * @ngdoc object
9030 * @name noDelay
9031 * @propertyOf ui.grid.class:ScrollEvent
9032 * @description most scroll events from the mouse or trackpad require delay to operate properly
9033 * set to false to eliminate delay. Useful for scroll events that the grid causes, such as scrolling to make a row visible.
9034 */
9035 self.withDelay = true;
9036
9037 self.sourceRowContainer = sourceRowContainer;
9038 self.sourceColContainer = sourceColContainer;
9039
9040 self.newScrollLeft = null;
9041 self.newScrollTop = null;
9042 self.x = null;
9043 self.y = null;
9044
9045 self.verticalScrollLength = -9999999;
9046 self.horizontalScrollLength = -999999;
9047
9048
9049 /**
9050 * @ngdoc function
9051 * @name fireThrottledScrollingEvent
9052 * @methodOf ui.grid.class:ScrollEvent
9053 * @description fires a throttled event using grid.api.core.raise.scrollEvent
9054 */
9055 self.fireThrottledScrollingEvent = gridUtil.throttle(function(sourceContainerId) {
9056 self.grid.scrollContainers(sourceContainerId, self);
9057 }, self.grid.options.wheelScrollThrottle, {trailing: true});
9058
9059 }
9060
9061
9062 /**
9063 * @ngdoc function
9064 * @name getNewScrollLeft
9065 * @methodOf ui.grid.class:ScrollEvent
9066 * @description returns newScrollLeft property if available; calculates a new value if it isn't
9067 */
9068 ScrollEvent.prototype.getNewScrollLeft = function(colContainer, viewport){
9069 var self = this;
9070
9071 if (!self.newScrollLeft){
9072 var scrollWidth = (colContainer.getCanvasWidth() - colContainer.getViewportWidth());
9073
9074 var oldScrollLeft = gridUtil.normalizeScrollLeft(viewport, self.grid);
9075
9076 var scrollXPercentage;
9077 if (typeof(self.x.percentage) !== 'undefined' && self.x.percentage !== undefined) {
9078 scrollXPercentage = self.x.percentage;
9079 }
9080 else if (typeof(self.x.pixels) !== 'undefined' && self.x.pixels !== undefined) {
9081 scrollXPercentage = self.x.percentage = (oldScrollLeft + self.x.pixels) / scrollWidth;
9082 }
9083 else {
9084 throw new Error("No percentage or pixel value provided for scroll event X axis");
9085 }
9086
9087 return Math.max(0, scrollXPercentage * scrollWidth);
9088 }
9089
9090 return self.newScrollLeft;
9091 };
9092
9093
9094 /**
9095 * @ngdoc function
9096 * @name getNewScrollTop
9097 * @methodOf ui.grid.class:ScrollEvent
9098 * @description returns newScrollTop property if available; calculates a new value if it isn't
9099 */
9100 ScrollEvent.prototype.getNewScrollTop = function(rowContainer, viewport){
9101 var self = this;
9102
9103
9104 if (!self.newScrollTop){
9105 var scrollLength = rowContainer.getVerticalScrollLength();
9106
9107 var oldScrollTop = viewport[0].scrollTop;
9108
9109 var scrollYPercentage;
9110 if (typeof(self.y.percentage) !== 'undefined' && self.y.percentage !== undefined) {
9111 scrollYPercentage = self.y.percentage;
9112 }
9113 else if (typeof(self.y.pixels) !== 'undefined' && self.y.pixels !== undefined) {
9114 scrollYPercentage = self.y.percentage = (oldScrollTop + self.y.pixels) / scrollLength;
9115 }
9116 else {
9117 throw new Error("No percentage or pixel value provided for scroll event Y axis");
9118 }
9119
9120 return Math.max(0, scrollYPercentage * scrollLength);
9121 }
9122
9123 return self.newScrollTop;
9124 };
9125
9126 ScrollEvent.prototype.atTop = function(scrollTop) {
9127 return (this.y && (this.y.percentage === 0 || this.verticalScrollLength < 0) && scrollTop === 0);
9128 };
9129
9130 ScrollEvent.prototype.atBottom = function(scrollTop) {
9131 return (this.y && (this.y.percentage === 1 || this.verticalScrollLength === 0) && scrollTop > 0);
9132 };
9133
9134 ScrollEvent.prototype.atLeft = function(scrollLeft) {
9135 return (this.x && (this.x.percentage === 0 || this.horizontalScrollLength < 0) && scrollLeft === 0);
9136 };
9137
9138 ScrollEvent.prototype.atRight = function(scrollLeft) {
9139 return (this.x && (this.x.percentage === 1 || this.horizontalScrollLength ===0) && scrollLeft > 0);
9140 };
9141
9142
9143 ScrollEvent.Sources = {
9144 ViewPortScroll: 'ViewPortScroll',
9145 RenderContainerMouseWheel: 'RenderContainerMouseWheel',
9146 RenderContainerTouchMove: 'RenderContainerTouchMove',
9147 Other: 99
9148 };
9149
9150 return ScrollEvent;
9151 }]);
9152
9153
9154
9155 })();
9156
9157 (function () {
9158 'use strict';
9159 /**
9160 * @ngdoc object
9161 * @name ui.grid.service:gridClassFactory
9162 *
9163 * @description factory to return dom specific instances of a grid
9164 *
9165 */
9166 angular.module('ui.grid').service('gridClassFactory', ['gridUtil', '$q', '$compile', '$templateCache', 'uiGridConstants', 'Grid', 'GridColumn', 'GridRow',
9167 function (gridUtil, $q, $compile, $templateCache, uiGridConstants, Grid, GridColumn, GridRow) {
9168
9169 var service = {
9170 /**
9171 * @ngdoc method
9172 * @name createGrid
9173 * @methodOf ui.grid.service:gridClassFactory
9174 * @description Creates a new grid instance. Each instance will have a unique id
9175 * @param {object} options An object map of options to pass into the created grid instance.
9176 * @returns {Grid} grid
9177 */
9178 createGrid : function(options) {
9179 options = (typeof(options) !== 'undefined') ? options : {};
9180 options.id = gridUtil.newId();
9181 var grid = new Grid(options);
9182
9183 // NOTE/TODO: rowTemplate should always be defined...
9184 if (grid.options.rowTemplate) {
9185 var rowTemplateFnPromise = $q.defer();
9186 grid.getRowTemplateFn = rowTemplateFnPromise.promise;
9187
9188 gridUtil.getTemplate(grid.options.rowTemplate)
9189 .then(
9190 function (template) {
9191 var rowTemplateFn = $compile(template);
9192 rowTemplateFnPromise.resolve(rowTemplateFn);
9193 },
9194 function (res) {
9195 // Todo handle response error here?
9196 throw new Error("Couldn't fetch/use row template '" + grid.options.rowTemplate + "'");
9197 });
9198 }
9199
9200 grid.registerColumnBuilder(service.defaultColumnBuilder);
9201
9202 // Row builder for custom row templates
9203 grid.registerRowBuilder(service.rowTemplateAssigner);
9204
9205 // Reset all rows to visible initially
9206 grid.registerRowsProcessor(function allRowsVisible(rows) {
9207 rows.forEach(function (row) {
9208 row.evaluateRowVisibility( true );
9209 }, 50);
9210
9211 return rows;
9212 });
9213
9214 grid.registerColumnsProcessor(function allColumnsVisible(columns) {
9215 columns.forEach(function (column) {
9216 column.visible = true;
9217 });
9218
9219 return columns;
9220 }, 50);
9221
9222 grid.registerColumnsProcessor(function(renderableColumns) {
9223 renderableColumns.forEach(function (column) {
9224 if (column.colDef.visible === false) {
9225 column.visible = false;
9226 }
9227 });
9228
9229 return renderableColumns;
9230 }, 50);
9231
9232
9233 grid.registerRowsProcessor(grid.searchRows, 100);
9234
9235 // Register the default row processor, it sorts rows by selected columns
9236 if (grid.options.externalSort && angular.isFunction(grid.options.externalSort)) {
9237 grid.registerRowsProcessor(grid.options.externalSort, 200);
9238 }
9239 else {
9240 grid.registerRowsProcessor(grid.sortByColumn, 200);
9241 }
9242
9243 return grid;
9244 },
9245
9246 /**
9247 * @ngdoc function
9248 * @name defaultColumnBuilder
9249 * @methodOf ui.grid.service:gridClassFactory
9250 * @description Processes designTime column definitions and applies them to col for the
9251 * core grid features
9252 * @param {object} colDef reference to column definition
9253 * @param {GridColumn} col reference to gridCol
9254 * @param {object} gridOptions reference to grid options
9255 */
9256 defaultColumnBuilder: function (colDef, col, gridOptions) {
9257
9258 var templateGetPromises = [];
9259
9260 // Abstracts the standard template processing we do for every template type.
9261 var processTemplate = function( templateType, providedType, defaultTemplate, filterType, tooltipType ) {
9262 if ( !colDef[templateType] ){
9263 col[providedType] = defaultTemplate;
9264 } else {
9265 col[providedType] = colDef[templateType];
9266 }
9267
9268 templateGetPromises.push(gridUtil.getTemplate(col[providedType])
9269 .then(
9270 function (template) {
9271 if ( angular.isFunction(template) ) { template = template(); }
9272 var tooltipCall = ( tooltipType === 'cellTooltip' ) ? 'col.cellTooltip(row,col)' : 'col.headerTooltip(col)';
9273 if ( tooltipType && col[tooltipType] === false ){
9274 template = template.replace(uiGridConstants.TOOLTIP, '');
9275 } else if ( tooltipType && col[tooltipType] ){
9276 template = template.replace(uiGridConstants.TOOLTIP, 'title="{{' + tooltipCall + ' CUSTOM_FILTERS }}"');
9277 }
9278
9279 if ( filterType ){
9280 col[templateType] = template.replace(uiGridConstants.CUSTOM_FILTERS, function() {
9281 return col[filterType] ? "|" + col[filterType] : "";
9282 });
9283 } else {
9284 col[templateType] = template;
9285 }
9286 },
9287 function (res) {
9288 throw new Error("Couldn't fetch/use colDef." + templateType + " '" + colDef[templateType] + "'");
9289 })
9290 );
9291
9292 };
9293
9294
9295 /**
9296 * @ngdoc property
9297 * @name cellTemplate
9298 * @propertyOf ui.grid.class:GridOptions.columnDef
9299 * @description a custom template for each cell in this column. The default
9300 * is ui-grid/uiGridCell. If you are using the cellNav feature, this template
9301 * must contain a div that can receive focus.
9302 *
9303 */
9304 processTemplate( 'cellTemplate', 'providedCellTemplate', 'ui-grid/uiGridCell', 'cellFilter', 'cellTooltip' );
9305 col.cellTemplatePromise = templateGetPromises[0];
9306
9307 /**
9308 * @ngdoc property
9309 * @name headerCellTemplate
9310 * @propertyOf ui.grid.class:GridOptions.columnDef
9311 * @description a custom template for the header for this column. The default
9312 * is ui-grid/uiGridHeaderCell
9313 *
9314 */
9315 processTemplate( 'headerCellTemplate', 'providedHeaderCellTemplate', 'ui-grid/uiGridHeaderCell', 'headerCellFilter', 'headerTooltip' );
9316
9317 /**
9318 * @ngdoc property
9319 * @name footerCellTemplate
9320 * @propertyOf ui.grid.class:GridOptions.columnDef
9321 * @description a custom template for the footer for this column. The default
9322 * is ui-grid/uiGridFooterCell
9323 *
9324 */
9325 processTemplate( 'footerCellTemplate', 'providedFooterCellTemplate', 'ui-grid/uiGridFooterCell', 'footerCellFilter' );
9326
9327 /**
9328 * @ngdoc property
9329 * @name filterHeaderTemplate
9330 * @propertyOf ui.grid.class:GridOptions.columnDef
9331 * @description a custom template for the filter input. The default is ui-grid/ui-grid-filter
9332 *
9333 */
9334 processTemplate( 'filterHeaderTemplate', 'providedFilterHeaderTemplate', 'ui-grid/ui-grid-filter' );
9335
9336 // Create a promise for the compiled element function
9337 col.compiledElementFnDefer = $q.defer();
9338
9339 return $q.all(templateGetPromises);
9340 },
9341
9342
9343 rowTemplateAssigner: function rowTemplateAssigner(row) {
9344 var grid = this;
9345
9346 // Row has no template assigned to it
9347 if (!row.rowTemplate) {
9348 // Use the default row template from the grid
9349 row.rowTemplate = grid.options.rowTemplate;
9350
9351 // Use the grid's function for fetching the compiled row template function
9352 row.getRowTemplateFn = grid.getRowTemplateFn;
9353 }
9354 // Row has its own template assigned
9355 else {
9356 // Create a promise for the compiled row template function
9357 var perRowTemplateFnPromise = $q.defer();
9358 row.getRowTemplateFn = perRowTemplateFnPromise.promise;
9359
9360 // Get the row template
9361 gridUtil.getTemplate(row.rowTemplate)
9362 .then(function (template) {
9363 // Compile the template
9364 var rowTemplateFn = $compile(template);
9365
9366 // Resolve the compiled template function promise
9367 perRowTemplateFnPromise.resolve(rowTemplateFn);
9368 },
9369 function (res) {
9370 // Todo handle response error here?
9371 throw new Error("Couldn't fetch/use row template '" + row.rowTemplate + "'");
9372 });
9373 }
9374
9375 return row.getRowTemplateFn;
9376 }
9377 };
9378
9379 //class definitions (moved to separate factories)
9380
9381 return service;
9382 }]);
9383
9384 })();
9385
9386 (function() {
9387
9388 var module = angular.module('ui.grid');
9389
9390 function escapeRegExp(str) {
9391 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
9392 }
9393
9394
9395 /**
9396 * @ngdoc service
9397 * @name ui.grid.service:rowSearcher
9398 *
9399 * @description Service for searching/filtering rows based on column value conditions.
9400 */
9401 module.service('rowSearcher', ['gridUtil', 'uiGridConstants', function (gridUtil, uiGridConstants) {
9402 var defaultCondition = uiGridConstants.filter.CONTAINS;
9403
9404 var rowSearcher = {};
9405
9406 /**
9407 * @ngdoc function
9408 * @name getTerm
9409 * @methodOf ui.grid.service:rowSearcher
9410 * @description Get the term from a filter
9411 * Trims leading and trailing whitespace
9412 * @param {object} filter object to use
9413 * @returns {object} Parsed term
9414 */
9415 rowSearcher.getTerm = function getTerm(filter) {
9416 if (typeof(filter.term) === 'undefined') { return filter.term; }
9417
9418 var term = filter.term;
9419
9420 // Strip leading and trailing whitespace if the term is a string
9421 if (typeof(term) === 'string') {
9422 term = term.trim();
9423 }
9424
9425 return term;
9426 };
9427
9428 /**
9429 * @ngdoc function
9430 * @name stripTerm
9431 * @methodOf ui.grid.service:rowSearcher
9432 * @description Remove leading and trailing asterisk (*) from the filter's term
9433 * @param {object} filter object to use
9434 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9435 */
9436 rowSearcher.stripTerm = function stripTerm(filter) {
9437 var term = rowSearcher.getTerm(filter);
9438
9439 if (typeof(term) === 'string') {
9440 return escapeRegExp(term.replace(/(^\*|\*$)/g, ''));
9441 }
9442 else {
9443 return term;
9444 }
9445 };
9446
9447
9448 /**
9449 * @ngdoc function
9450 * @name guessCondition
9451 * @methodOf ui.grid.service:rowSearcher
9452 * @description Guess the condition for a filter based on its term
9453 * <br>
9454 * Defaults to STARTS_WITH. Uses CONTAINS for strings beginning and ending with *s (*bob*).
9455 * Uses STARTS_WITH for strings ending with * (bo*). Uses ENDS_WITH for strings starting with * (*ob).
9456 * @param {object} filter object to use
9457 * @returns {uiGridConstants.filter<int>} Value representing the condition constant value
9458 */
9459 rowSearcher.guessCondition = function guessCondition(filter) {
9460 if (typeof(filter.term) === 'undefined' || !filter.term) {
9461 return defaultCondition;
9462 }
9463
9464 var term = rowSearcher.getTerm(filter);
9465
9466 if (/\*/.test(term)) {
9467 var regexpFlags = '';
9468 if (!filter.flags || !filter.flags.caseSensitive) {
9469 regexpFlags += 'i';
9470 }
9471
9472 var reText = term.replace(/(\\)?\*/g, function ($0, $1) { return $1 ? $0 : '[\\s\\S]*?'; });
9473 return new RegExp('^' + reText + '$', regexpFlags);
9474 }
9475 // Otherwise default to default condition
9476 else {
9477 return defaultCondition;
9478 }
9479 };
9480
9481
9482 /**
9483 * @ngdoc function
9484 * @name setupFilters
9485 * @methodOf ui.grid.service:rowSearcher
9486 * @description For a given columns filters (either col.filters, or [col.filter] can be passed in),
9487 * do all the parsing and pre-processing and store that data into a new filters object. The object
9488 * has the condition, the flags, the stripped term, and a parsed reg exp if there was one.
9489 *
9490 * We could use a forEach in here, since it's much less performance sensitive, but since we're using
9491 * for loops everywhere else in this module...
9492 *
9493 * @param {array} filters the filters from the column (col.filters or [col.filter])
9494 * @returns {array} An array of parsed/preprocessed filters
9495 */
9496 rowSearcher.setupFilters = function setupFilters( filters ){
9497 var newFilters = [];
9498
9499 var filtersLength = filters.length;
9500 for ( var i = 0; i < filtersLength; i++ ){
9501 var filter = filters[i];
9502
9503 if ( filter.noTerm || !gridUtil.isNullOrUndefined(filter.term) ){
9504 var newFilter = {};
9505
9506 var regexpFlags = '';
9507 if (!filter.flags || !filter.flags.caseSensitive) {
9508 regexpFlags += 'i';
9509 }
9510
9511 if ( !gridUtil.isNullOrUndefined(filter.term) ){
9512 // it is possible to have noTerm. We don't need to copy that across, it was just a flag to avoid
9513 // getting the filter ignored if the filter was a function that didn't use a term
9514 newFilter.term = rowSearcher.stripTerm(filter);
9515 }
9516
9517 if ( filter.condition ){
9518 newFilter.condition = filter.condition;
9519 } else {
9520 newFilter.condition = rowSearcher.guessCondition(filter);
9521 }
9522
9523 newFilter.flags = angular.extend( { caseSensitive: false, date: false }, filter.flags );
9524
9525 if (newFilter.condition === uiGridConstants.filter.STARTS_WITH) {
9526 newFilter.startswithRE = new RegExp('^' + newFilter.term, regexpFlags);
9527 }
9528
9529 if (newFilter.condition === uiGridConstants.filter.ENDS_WITH) {
9530 newFilter.endswithRE = new RegExp(newFilter.term + '$', regexpFlags);
9531 }
9532
9533 if (newFilter.condition === uiGridConstants.filter.CONTAINS) {
9534 newFilter.containsRE = new RegExp(newFilter.term, regexpFlags);
9535 }
9536
9537 if (newFilter.condition === uiGridConstants.filter.EXACT) {
9538 newFilter.exactRE = new RegExp('^' + newFilter.term + '$', regexpFlags);
9539 }
9540
9541 newFilters.push(newFilter);
9542 }
9543 }
9544 return newFilters;
9545 };
9546
9547
9548 /**
9549 * @ngdoc function
9550 * @name runColumnFilter
9551 * @methodOf ui.grid.service:rowSearcher
9552 * @description Runs a single pre-parsed filter against a cell, returning true
9553 * if the cell matches that one filter.
9554 *
9555 * @param {Grid} grid the grid we're working against
9556 * @param {GridRow} row the row we're matching against
9557 * @param {GridCol} column the column that we're working against
9558 * @param {object} filter the specific, preparsed, filter that we want to test
9559 * @returns {boolean} true if we match (row stays visible)
9560 */
9561 rowSearcher.runColumnFilter = function runColumnFilter(grid, row, column, filter) {
9562 // Cache typeof condition
9563 var conditionType = typeof(filter.condition);
9564
9565 // Term to search for.
9566 var term = filter.term;
9567
9568 // Get the column value for this row
9569 var value;
9570 if ( column.filterCellFiltered ){
9571 value = grid.getCellDisplayValue(row, column);
9572 } else {
9573 value = grid.getCellValue(row, column);
9574 }
9575
9576
9577 // If the filter's condition is a RegExp, then use it
9578 if (filter.condition instanceof RegExp) {
9579 return filter.condition.test(value);
9580 }
9581
9582 // If the filter's condition is a function, run it
9583 if (conditionType === 'function') {
9584 return filter.condition(term, value, row, column);
9585 }
9586
9587 if (filter.startswithRE) {
9588 return filter.startswithRE.test(value);
9589 }
9590
9591 if (filter.endswithRE) {
9592 return filter.endswithRE.test(value);
9593 }
9594
9595 if (filter.containsRE) {
9596 return filter.containsRE.test(value);
9597 }
9598
9599 if (filter.exactRE) {
9600 return filter.exactRE.test(value);
9601 }
9602
9603 if (filter.condition === uiGridConstants.filter.NOT_EQUAL) {
9604 var regex = new RegExp('^' + term + '$');
9605 return !regex.exec(value);
9606 }
9607
9608 if (typeof(value) === 'number' && typeof(term) === 'string' ){
9609 // if the term has a decimal in it, it comes through as '9\.4', we need to take out the \
9610 // the same for negative numbers
9611 // TODO: I suspect the right answer is to look at escapeRegExp at the top of this code file, maybe it's not needed?
9612 var tempFloat = parseFloat(term.replace(/\\\./,'.').replace(/\\\-/,'-'));
9613 if (!isNaN(tempFloat)) {
9614 term = tempFloat;
9615 }
9616 }
9617
9618 if (filter.flags.date === true) {
9619 value = new Date(value);
9620 // If the term has a dash in it, it comes through as '\-' -- we need to take out the '\'.
9621 term = new Date(term.replace(/\\/g, ''));
9622 }
9623
9624 if (filter.condition === uiGridConstants.filter.GREATER_THAN) {
9625 return (value > term);
9626 }
9627
9628 if (filter.condition === uiGridConstants.filter.GREATER_THAN_OR_EQUAL) {
9629 return (value >= term);
9630 }
9631
9632 if (filter.condition === uiGridConstants.filter.LESS_THAN) {
9633 return (value < term);
9634 }
9635
9636 if (filter.condition === uiGridConstants.filter.LESS_THAN_OR_EQUAL) {
9637 return (value <= term);
9638 }
9639
9640 return true;
9641 };
9642
9643
9644 /**
9645 * @ngdoc boolean
9646 * @name useExternalFiltering
9647 * @propertyOf ui.grid.class:GridOptions
9648 * @description False by default. When enabled, this setting suppresses the internal filtering.
9649 * All UI logic will still operate, allowing filter conditions to be set and modified.
9650 *
9651 * The external filter logic can listen for the `filterChange` event, which fires whenever
9652 * a filter has been adjusted.
9653 */
9654 /**
9655 * @ngdoc function
9656 * @name searchColumn
9657 * @methodOf ui.grid.service:rowSearcher
9658 * @description Process provided filters on provided column against a given row. If the row meets
9659 * the conditions on all the filters, return true.
9660 * @param {Grid} grid Grid to search in
9661 * @param {GridRow} row Row to search on
9662 * @param {GridCol} column Column with the filters to use
9663 * @param {array} filters array of pre-parsed/preprocessed filters to apply
9664 * @returns {boolean} Whether the column matches or not.
9665 */
9666 rowSearcher.searchColumn = function searchColumn(grid, row, column, filters) {
9667 if (grid.options.useExternalFiltering) {
9668 return true;
9669 }
9670
9671 var filtersLength = filters.length;
9672 for (var i = 0; i < filtersLength; i++) {
9673 var filter = filters[i];
9674
9675 var ret = rowSearcher.runColumnFilter(grid, row, column, filter);
9676 if (!ret) {
9677 return false;
9678 }
9679 }
9680
9681 return true;
9682 };
9683
9684
9685 /**
9686 * @ngdoc function
9687 * @name search
9688 * @methodOf ui.grid.service:rowSearcher
9689 * @description Run a search across the given rows and columns, marking any rows that don't
9690 * match the stored col.filters or col.filter as invisible.
9691 * @param {Grid} grid Grid instance to search inside
9692 * @param {Array[GridRow]} rows GridRows to filter
9693 * @param {Array[GridColumn]} columns GridColumns with filters to process
9694 */
9695 rowSearcher.search = function search(grid, rows, columns) {
9696 /*
9697 * Added performance optimisations into this code base, as this logic creates deeply nested
9698 * loops and is therefore very performance sensitive. In particular, avoiding forEach as
9699 * this impacts some browser optimisers (particularly Chrome), using iterators instead
9700 */
9701
9702 // Don't do anything if we weren't passed any rows
9703 if (!rows) {
9704 return;
9705 }
9706
9707 // don't filter if filtering currently disabled
9708 if (!grid.options.enableFiltering){
9709 return rows;
9710 }
9711
9712 // Build list of filters to apply
9713 var filterData = [];
9714
9715 var colsLength = columns.length;
9716
9717 var hasTerm = function( filters ) {
9718 var hasTerm = false;
9719
9720 filters.forEach( function (filter) {
9721 if ( !gridUtil.isNullOrUndefined(filter.term) && filter.term !== '' || filter.noTerm ){
9722 hasTerm = true;
9723 }
9724 });
9725
9726 return hasTerm;
9727 };
9728
9729 for (var i = 0; i < colsLength; i++) {
9730 var col = columns[i];
9731
9732 if (typeof(col.filters) !== 'undefined' && hasTerm(col.filters) ) {
9733 filterData.push( { col: col, filters: rowSearcher.setupFilters(col.filters) } );
9734 }
9735 }
9736
9737 if (filterData.length > 0) {
9738 // define functions outside the loop, performance optimisation
9739 var foreachRow = function(grid, row, col, filters){
9740 if ( row.visible && !rowSearcher.searchColumn(grid, row, col, filters) ) {
9741 row.visible = false;
9742 }
9743 };
9744
9745 var foreachFilterCol = function(grid, filterData){
9746 var rowsLength = rows.length;
9747 for ( var i = 0; i < rowsLength; i++){
9748 foreachRow(grid, rows[i], filterData.col, filterData.filters);
9749 }
9750 };
9751
9752 // nested loop itself - foreachFilterCol, which in turn calls foreachRow
9753 var filterDataLength = filterData.length;
9754 for ( var j = 0; j < filterDataLength; j++){
9755 foreachFilterCol( grid, filterData[j] );
9756 }
9757
9758 if (grid.api.core.raise.rowsVisibleChanged) {
9759 grid.api.core.raise.rowsVisibleChanged();
9760 }
9761
9762 // drop any invisible rows
9763 // keeping these, as needed with filtering for trees - we have to come back and make parent nodes visible if child nodes are selected in the filter
9764 // rows = rows.filter(function(row){ return row.visible; });
9765
9766 }
9767
9768 return rows;
9769 };
9770
9771 return rowSearcher;
9772 }]);
9773
9774 })();
9775
9776 (function() {
9777
9778 var module = angular.module('ui.grid');
9779
9780 /**
9781 * @ngdoc object
9782 * @name ui.grid.class:RowSorter
9783 * @description RowSorter provides the default sorting mechanisms,
9784 * including guessing column types and applying appropriate sort
9785 * algorithms
9786 *
9787 */
9788
9789 module.service('rowSorter', ['$parse', 'uiGridConstants', function ($parse, uiGridConstants) {
9790 var currencyRegexStr =
9791 '(' +
9792 uiGridConstants.CURRENCY_SYMBOLS
9793 .map(function (a) { return '\\' + a; }) // Escape all the currency symbols ($ at least will jack up this regex)
9794 .join('|') + // Join all the symbols together with |s
9795 ')?';
9796
9797 // /^[-+]?[£$¤¥]?[\d,.]+%?$/
9798 var numberStrRegex = new RegExp('^[-+]?' + currencyRegexStr + '[\\d,.]+' + currencyRegexStr + '%?$');
9799
9800 var rowSorter = {
9801 // Cache of sorting functions. Once we create them, we don't want to keep re-doing it
9802 // this takes a piece of data from the cell and tries to determine its type and what sorting
9803 // function to use for it
9804 colSortFnCache: {}
9805 };
9806
9807
9808 /**
9809 * @ngdoc method
9810 * @methodOf ui.grid.class:RowSorter
9811 * @name guessSortFn
9812 * @description Assigns a sort function to use based on the itemType in the column
9813 * @param {string} itemType one of 'number', 'boolean', 'string', 'date', 'object'. And
9814 * error will be thrown for any other type.
9815 * @returns {function} a sort function that will sort that type
9816 */
9817 rowSorter.guessSortFn = function guessSortFn(itemType) {
9818 switch (itemType) {
9819 case "number":
9820 return rowSorter.sortNumber;
9821 case "numberStr":
9822 return rowSorter.sortNumberStr;
9823 case "boolean":
9824 return rowSorter.sortBool;
9825 case "string":
9826 return rowSorter.sortAlpha;
9827 case "date":
9828 return rowSorter.sortDate;
9829 case "object":
9830 return rowSorter.basicSort;
9831 default:
9832 throw new Error('No sorting function found for type:' + itemType);
9833 }
9834 };
9835
9836
9837 /**
9838 * @ngdoc method
9839 * @methodOf ui.grid.class:RowSorter
9840 * @name handleNulls
9841 * @description Sorts nulls and undefined to the bottom (top when
9842 * descending). Called by each of the internal sorters before
9843 * attempting to sort. Note that this method is available on the core api
9844 * via gridApi.core.sortHandleNulls
9845 * @param {object} a sort value a
9846 * @param {object} b sort value b
9847 * @returns {number} null if there were no nulls/undefineds, otherwise returns
9848 * a sort value that should be passed back from the sort function
9849 */
9850 rowSorter.handleNulls = function handleNulls(a, b) {
9851 // We want to allow zero values and false values to be evaluated in the sort function
9852 if ((!a && a !== 0 && a !== false) || (!b && b !== 0 && b !== false)) {
9853 // We want to force nulls and such to the bottom when we sort... which effectively is "greater than"
9854 if ((!a && a !== 0 && a !== false) && (!b && b !== 0 && b !== false)) {
9855 return 0;
9856 }
9857 else if (!a && a !== 0 && a !== false) {
9858 return 1;
9859 }
9860 else if (!b && b !== 0 && b !== false) {
9861 return -1;
9862 }
9863 }
9864 return null;
9865 };
9866
9867
9868 /**
9869 * @ngdoc method
9870 * @methodOf ui.grid.class:RowSorter
9871 * @name basicSort
9872 * @description Sorts any values that provide the < method, including strings
9873 * or numbers. Handles nulls and undefined through calling handleNulls
9874 * @param {object} a sort value a
9875 * @param {object} b sort value b
9876 * @returns {number} normal sort function, returns -ve, 0, +ve
9877 */
9878 rowSorter.basicSort = function basicSort(a, b) {
9879 var nulls = rowSorter.handleNulls(a, b);
9880 if ( nulls !== null ){
9881 return nulls;
9882 } else {
9883 if (a === b) {
9884 return 0;
9885 }
9886 if (a < b) {
9887 return -1;
9888 }
9889 return 1;
9890 }
9891 };
9892
9893
9894 /**
9895 * @ngdoc method
9896 * @methodOf ui.grid.class:RowSorter
9897 * @name sortNumber
9898 * @description Sorts numerical values. Handles nulls and undefined through calling handleNulls
9899 * @param {object} a sort value a
9900 * @param {object} b sort value b
9901 * @returns {number} normal sort function, returns -ve, 0, +ve
9902 */
9903 rowSorter.sortNumber = function sortNumber(a, b) {
9904 var nulls = rowSorter.handleNulls(a, b);
9905 if ( nulls !== null ){
9906 return nulls;
9907 } else {
9908 return a - b;
9909 }
9910 };
9911
9912
9913 /**
9914 * @ngdoc method
9915 * @methodOf ui.grid.class:RowSorter
9916 * @name sortNumberStr
9917 * @description Sorts numerical values that are stored in a string (i.e. parses them to numbers first).
9918 * Handles nulls and undefined through calling handleNulls
9919 * @param {object} a sort value a
9920 * @param {object} b sort value b
9921 * @returns {number} normal sort function, returns -ve, 0, +ve
9922 */
9923 rowSorter.sortNumberStr = function sortNumberStr(a, b) {
9924 var nulls = rowSorter.handleNulls(a, b);
9925 if ( nulls !== null ){
9926 return nulls;
9927 } else {
9928 var numA, // The parsed number form of 'a'
9929 numB, // The parsed number form of 'b'
9930 badA = false,
9931 badB = false;
9932
9933 // Try to parse 'a' to a float
9934 numA = parseFloat(a.replace(/[^0-9.-]/g, ''));
9935
9936 // If 'a' couldn't be parsed to float, flag it as bad
9937 if (isNaN(numA)) {
9938 badA = true;
9939 }
9940
9941 // Try to parse 'b' to a float
9942 numB = parseFloat(b.replace(/[^0-9.-]/g, ''));
9943
9944 // If 'b' couldn't be parsed to float, flag it as bad
9945 if (isNaN(numB)) {
9946 badB = true;
9947 }
9948
9949 // We want bad ones to get pushed to the bottom... which effectively is "greater than"
9950 if (badA && badB) {
9951 return 0;
9952 }
9953
9954 if (badA) {
9955 return 1;
9956 }
9957
9958 if (badB) {
9959 return -1;
9960 }
9961
9962 return numA - numB;
9963 }
9964 };
9965
9966
9967 /**
9968 * @ngdoc method
9969 * @methodOf ui.grid.class:RowSorter
9970 * @name sortAlpha
9971 * @description Sorts string values. Handles nulls and undefined through calling handleNulls
9972 * @param {object} a sort value a
9973 * @param {object} b sort value b
9974 * @returns {number} normal sort function, returns -ve, 0, +ve
9975 */
9976 rowSorter.sortAlpha = function sortAlpha(a, b) {
9977 var nulls = rowSorter.handleNulls(a, b);
9978 if ( nulls !== null ){
9979 return nulls;
9980 } else {
9981 var strA = a.toString().toLowerCase(),
9982 strB = b.toString().toLowerCase();
9983
9984 return strA === strB ? 0 : (strA < strB ? -1 : 1);
9985 }
9986 };
9987
9988
9989 /**
9990 * @ngdoc method
9991 * @methodOf ui.grid.class:RowSorter
9992 * @name sortDate
9993 * @description Sorts date values. Handles nulls and undefined through calling handleNulls.
9994 * Handles date strings by converting to Date object if not already an instance of Date
9995 * @param {object} a sort value a
9996 * @param {object} b sort value b
9997 * @returns {number} normal sort function, returns -ve, 0, +ve
9998 */
9999 rowSorter.sortDate = function sortDate(a, b) {
10000 var nulls = rowSorter.handleNulls(a, b);
10001 if ( nulls !== null ){
10002 return nulls;
10003 } else {
10004 if (!(a instanceof Date)) {
10005 a = new Date(a);
10006 }
10007 if (!(b instanceof Date)){
10008 b = new Date(b);
10009 }
10010 var timeA = a.getTime(),
10011 timeB = b.getTime();
10012
10013 return timeA === timeB ? 0 : (timeA < timeB ? -1 : 1);
10014 }
10015 };
10016
10017
10018 /**
10019 * @ngdoc method
10020 * @methodOf ui.grid.class:RowSorter
10021 * @name sortBool
10022 * @description Sorts boolean values, true is considered larger than false.
10023 * Handles nulls and undefined through calling handleNulls
10024 * @param {object} a sort value a
10025 * @param {object} b sort value b
10026 * @returns {number} normal sort function, returns -ve, 0, +ve
10027 */
10028 rowSorter.sortBool = function sortBool(a, b) {
10029 var nulls = rowSorter.handleNulls(a, b);
10030 if ( nulls !== null ){
10031 return nulls;
10032 } else {
10033 if (a && b) {
10034 return 0;
10035 }
10036
10037 if (!a && !b) {
10038 return 0;
10039 }
10040 else {
10041 return a ? 1 : -1;
10042 }
10043 }
10044 };
10045
10046
10047 /**
10048 * @ngdoc method
10049 * @methodOf ui.grid.class:RowSorter
10050 * @name getSortFn
10051 * @description Get the sort function for the column. Looks first in
10052 * rowSorter.colSortFnCache using the column name, failing that it
10053 * looks at col.sortingAlgorithm (and puts it in the cache), failing that
10054 * it guesses the sort algorithm based on the data type.
10055 *
10056 * The cache currently seems a bit pointless, as none of the work we do is
10057 * processor intensive enough to need caching. Presumably in future we might
10058 * inspect the row data itself to guess the sort function, and in that case
10059 * it would make sense to have a cache, the infrastructure is in place to allow
10060 * that.
10061 *
10062 * @param {Grid} grid the grid to consider
10063 * @param {GridCol} col the column to find a function for
10064 * @param {array} rows an array of grid rows. Currently unused, but presumably in future
10065 * we might inspect the rows themselves to decide what sort of data might be there
10066 * @returns {function} the sort function chosen for the column
10067 */
10068 rowSorter.getSortFn = function getSortFn(grid, col, rows) {
10069 var sortFn, item;
10070
10071 // See if we already figured out what to use to sort the column and have it in the cache
10072 if (rowSorter.colSortFnCache[col.colDef.name]) {
10073 sortFn = rowSorter.colSortFnCache[col.colDef.name];
10074 }
10075 // If the column has its OWN sorting algorithm, use that
10076 else if (col.sortingAlgorithm !== undefined) {
10077 sortFn = col.sortingAlgorithm;
10078 rowSorter.colSortFnCache[col.colDef.name] = col.sortingAlgorithm;
10079 }
10080 // Always default to sortAlpha when sorting after a cellFilter
10081 else if ( col.sortCellFiltered && col.cellFilter ){
10082 sortFn = rowSorter.sortAlpha;
10083 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10084 }
10085 // Try and guess what sort function to use
10086 else {
10087 // Guess the sort function
10088 sortFn = rowSorter.guessSortFn(col.colDef.type);
10089
10090 // If we found a sort function, cache it
10091 if (sortFn) {
10092 rowSorter.colSortFnCache[col.colDef.name] = sortFn;
10093 }
10094 else {
10095 // We assign the alpha sort because anything that is null/undefined will never get passed to
10096 // the actual sorting function. It will get caught in our null check and returned to be sorted
10097 // down to the bottom
10098 sortFn = rowSorter.sortAlpha;
10099 }
10100 }
10101
10102 return sortFn;
10103 };
10104
10105
10106
10107 /**
10108 * @ngdoc method
10109 * @methodOf ui.grid.class:RowSorter
10110 * @name prioritySort
10111 * @description Used where multiple columns are present in the sort criteria,
10112 * we determine which column should take precedence in the sort by sorting
10113 * the columns based on their sort.priority
10114 *
10115 * @param {gridColumn} a column a
10116 * @param {gridColumn} b column b
10117 * @returns {number} normal sort function, returns -ve, 0, +ve
10118 */
10119 rowSorter.prioritySort = function (a, b) {
10120 // Both columns have a sort priority
10121 if (a.sort.priority !== undefined && b.sort.priority !== undefined) {
10122 // A is higher priority
10123 if (a.sort.priority < b.sort.priority) {
10124 return -1;
10125 }
10126 // Equal
10127 else if (a.sort.priority === b.sort.priority) {
10128 return 0;
10129 }
10130 // B is higher
10131 else {
10132 return 1;
10133 }
10134 }
10135 // Only A has a priority
10136 else if (a.sort.priority || a.sort.priority === 0) {
10137 return -1;
10138 }
10139 // Only B has a priority
10140 else if (b.sort.priority || b.sort.priority === 0) {
10141 return 1;
10142 }
10143 // Neither has a priority
10144 else {
10145 return 0;
10146 }
10147 };
10148
10149
10150 /**
10151 * @ngdoc object
10152 * @name useExternalSorting
10153 * @propertyOf ui.grid.class:GridOptions
10154 * @description Prevents the internal sorting from executing. Events will
10155 * still be fired when the sort changes, and the sort information on
10156 * the columns will be updated, allowing an external sorter (for example,
10157 * server sorting) to be implemented. Defaults to false.
10158 *
10159 */
10160 /**
10161 * @ngdoc method
10162 * @methodOf ui.grid.class:RowSorter
10163 * @name sort
10164 * @description sorts the grid
10165 * @param {Object} grid the grid itself
10166 * @param {array} rows the rows to be sorted
10167 * @param {array} columns the columns in which to look
10168 * for sort criteria
10169 * @returns {array} sorted rows
10170 */
10171 rowSorter.sort = function rowSorterSort(grid, rows, columns) {
10172 // first make sure we are even supposed to do work
10173 if (!rows) {
10174 return;
10175 }
10176
10177 if (grid.options.useExternalSorting){
10178 return rows;
10179 }
10180
10181 // Build the list of columns to sort by
10182 var sortCols = [];
10183 columns.forEach(function (col) {
10184 if (col.sort && !col.sort.ignoreSort && col.sort.direction && (col.sort.direction === uiGridConstants.ASC || col.sort.direction === uiGridConstants.DESC)) {
10185 sortCols.push(col);
10186 }
10187 });
10188
10189 // Sort the "sort columns" by their sort priority
10190 sortCols = sortCols.sort(rowSorter.prioritySort);
10191
10192 // Now rows to sort by, maintain original order
10193 if (sortCols.length === 0) {
10194 return rows;
10195 }
10196
10197 // Re-usable variables
10198 var col, direction;
10199
10200 // put a custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10201 var setIndex = function( row, idx ){
10202 row.entity.$$uiGridIndex = idx;
10203 };
10204 rows.forEach(setIndex);
10205
10206 // IE9-11 HACK.... the 'rows' variable would be empty where we call rowSorter.getSortFn(...) below. We have to use a separate reference
10207 // var d = data.slice(0);
10208 var r = rows.slice(0);
10209
10210 // Now actually sort the data
10211 var rowSortFn = function (rowA, rowB) {
10212 var tem = 0,
10213 idx = 0,
10214 sortFn;
10215
10216 while (tem === 0 && idx < sortCols.length) {
10217 // grab the metadata for the rest of the logic
10218 col = sortCols[idx];
10219 direction = sortCols[idx].sort.direction;
10220
10221 sortFn = rowSorter.getSortFn(grid, col, r);
10222
10223 var propA, propB;
10224
10225 if ( col.sortCellFiltered ){
10226 propA = grid.getCellDisplayValue(rowA, col);
10227 propB = grid.getCellDisplayValue(rowB, col);
10228 } else {
10229 propA = grid.getCellValue(rowA, col);
10230 propB = grid.getCellValue(rowB, col);
10231 }
10232
10233 tem = sortFn(propA, propB, rowA, rowB, direction);
10234
10235 idx++;
10236 }
10237
10238 // Chrome doesn't implement a stable sort function. If our sort returns 0
10239 // (i.e. the items are equal), and we're at the last sort column in the list,
10240 // then return the previous order using our custom
10241 // index variable
10242 if (tem === 0 ) {
10243 return rowA.entity.$$uiGridIndex - rowB.entity.$$uiGridIndex;
10244 }
10245
10246 // Made it this far, we don't have to worry about null & undefined
10247 if (direction === uiGridConstants.ASC) {
10248 return tem;
10249 } else {
10250 return 0 - tem;
10251 }
10252 };
10253
10254 var newRows = rows.sort(rowSortFn);
10255
10256 // remove the custom index field on each row, used to make a stable sort out of unstable sorts (e.g. Chrome)
10257 var clearIndex = function( row, idx ){
10258 delete row.entity.$$uiGridIndex;
10259 };
10260 rows.forEach(clearIndex);
10261
10262 return newRows;
10263 };
10264
10265 return rowSorter;
10266 }]);
10267
10268 })();
10269
10270 (function() {
10271
10272 var module = angular.module('ui.grid');
10273
10274 var bindPolyfill;
10275 if (typeof Function.prototype.bind !== "function") {
10276 bindPolyfill = function() {
10277 var slice = Array.prototype.slice;
10278 return function(context) {
10279 var fn = this,
10280 args = slice.call(arguments, 1);
10281 if (args.length) {
10282 return function() {
10283 return arguments.length ? fn.apply(context, args.concat(slice.call(arguments))) : fn.apply(context, args);
10284 };
10285 }
10286 return function() {
10287 return arguments.length ? fn.apply(context, arguments) : fn.call(context);
10288 };
10289 };
10290 };
10291 }
10292
10293 function getStyles (elem) {
10294 var e = elem;
10295 if (typeof(e.length) !== 'undefined' && e.length) {
10296 e = elem[0];
10297 }
10298
10299 return e.ownerDocument.defaultView.getComputedStyle(e, null);
10300 }
10301
10302 var rnumnonpx = new RegExp( "^(" + (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source + ")(?!px)[a-z%]+$", "i" ),
10303 // swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
10304 // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
10305 rdisplayswap = /^(block|none|table(?!-c[ea]).+)/,
10306 cssShow = { position: "absolute", visibility: "hidden", display: "block" };
10307
10308 function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
10309 var i = extra === ( isBorderBox ? 'border' : 'content' ) ?
10310 // If we already have the right measurement, avoid augmentation
10311 4 :
10312 // Otherwise initialize for horizontal or vertical properties
10313 name === 'width' ? 1 : 0,
10314
10315 val = 0;
10316
10317 var sides = ['Top', 'Right', 'Bottom', 'Left'];
10318
10319 for ( ; i < 4; i += 2 ) {
10320 var side = sides[i];
10321 // dump('side', side);
10322
10323 // both box models exclude margin, so add it if we want it
10324 if ( extra === 'margin' ) {
10325 var marg = parseFloat(styles[extra + side]);
10326 if (!isNaN(marg)) {
10327 val += marg;
10328 }
10329 }
10330 // dump('val1', val);
10331
10332 if ( isBorderBox ) {
10333 // border-box includes padding, so remove it if we want content
10334 if ( extra === 'content' ) {
10335 var padd = parseFloat(styles['padding' + side]);
10336 if (!isNaN(padd)) {
10337 val -= padd;
10338 // dump('val2', val);
10339 }
10340 }
10341
10342 // at this point, extra isn't border nor margin, so remove border
10343 if ( extra !== 'margin' ) {
10344 var bordermarg = parseFloat(styles['border' + side + 'Width']);
10345 if (!isNaN(bordermarg)) {
10346 val -= bordermarg;
10347 // dump('val3', val);
10348 }
10349 }
10350 }
10351 else {
10352 // at this point, extra isn't content, so add padding
10353 var nocontentPad = parseFloat(styles['padding' + side]);
10354 if (!isNaN(nocontentPad)) {
10355 val += nocontentPad;
10356 // dump('val4', val);
10357 }
10358
10359 // at this point, extra isn't content nor padding, so add border
10360 if ( extra !== 'padding') {
10361 var nocontentnopad = parseFloat(styles['border' + side + 'Width']);
10362 if (!isNaN(nocontentnopad)) {
10363 val += nocontentnopad;
10364 // dump('val5', val);
10365 }
10366 }
10367 }
10368 }
10369
10370 // dump('augVal', val);
10371
10372 return val;
10373 }
10374
10375 function getWidthOrHeight( elem, name, extra ) {
10376 // Start with offset property, which is equivalent to the border-box value
10377 var valueIsBorderBox = true,
10378 val, // = name === 'width' ? elem.offsetWidth : elem.offsetHeight,
10379 styles = getStyles(elem),
10380 isBorderBox = styles['boxSizing'] === 'border-box';
10381
10382 // some non-html elements return undefined for offsetWidth, so check for null/undefined
10383 // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
10384 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
10385 if ( val <= 0 || val == null ) {
10386 // Fall back to computed then uncomputed css if necessary
10387 val = styles[name];
10388 if ( val < 0 || val == null ) {
10389 val = elem.style[ name ];
10390 }
10391
10392 // Computed unit is not pixels. Stop here and return.
10393 if ( rnumnonpx.test(val) ) {
10394 return val;
10395 }
10396
10397 // we need the check for style in case a browser which returns unreliable values
10398 // for getComputedStyle silently falls back to the reliable elem.style
10399 valueIsBorderBox = isBorderBox &&
10400 ( true || val === elem.style[ name ] ); // use 'true' instead of 'support.boxSizingReliable()'
10401
10402 // Normalize "", auto, and prepare for extra
10403 val = parseFloat( val ) || 0;
10404 }
10405
10406 // use the active box-sizing model to add/subtract irrelevant styles
10407 var ret = ( val +
10408 augmentWidthOrHeight(
10409 elem,
10410 name,
10411 extra || ( isBorderBox ? "border" : "content" ),
10412 valueIsBorderBox,
10413 styles
10414 )
10415 );
10416
10417 // dump('ret', ret, val);
10418 return ret;
10419 }
10420
10421 function getLineHeight(elm) {
10422 elm = angular.element(elm)[0];
10423 var parent = elm.parentElement;
10424
10425 if (!parent) {
10426 parent = document.getElementsByTagName('body')[0];
10427 }
10428
10429 return parseInt( getStyles(parent).fontSize ) || parseInt( getStyles(elm).fontSize ) || 16;
10430 }
10431
10432 var uid = ['0', '0', '0', '0'];
10433 var uidPrefix = 'uiGrid-';
10434
10435 /**
10436 * @ngdoc service
10437 * @name ui.grid.service:GridUtil
10438 *
10439 * @description Grid utility functions
10440 */
10441 module.service('gridUtil', ['$log', '$window', '$document', '$http', '$templateCache', '$timeout', '$interval', '$injector', '$q', '$interpolate', 'uiGridConstants',
10442 function ($log, $window, $document, $http, $templateCache, $timeout, $interval, $injector, $q, $interpolate, uiGridConstants) {
10443 var s = {
10444
10445 augmentWidthOrHeight: augmentWidthOrHeight,
10446
10447 getStyles: getStyles,
10448
10449 /**
10450 * @ngdoc method
10451 * @name createBoundedWrapper
10452 * @methodOf ui.grid.service:GridUtil
10453 *
10454 * @param {object} Object to bind 'this' to
10455 * @param {method} Method to bind
10456 * @returns {Function} The wrapper that performs the binding
10457 *
10458 * @description
10459 * Binds given method to given object.
10460 *
10461 * By means of a wrapper, ensures that ``method`` is always bound to
10462 * ``object`` regardless of its calling environment.
10463 * Iow, inside ``method``, ``this`` always points to ``object``.
10464 *
10465 * See http://alistapart.com/article/getoutbindingsituations
10466 *
10467 */
10468 createBoundedWrapper: function(object, method) {
10469 return function() {
10470 return method.apply(object, arguments);
10471 };
10472 },
10473
10474
10475 /**
10476 * @ngdoc method
10477 * @name readableColumnName
10478 * @methodOf ui.grid.service:GridUtil
10479 *
10480 * @param {string} columnName Column name as a string
10481 * @returns {string} Column name appropriately capitalized and split apart
10482 *
10483 @example
10484 <example module="app">
10485 <file name="app.js">
10486 var app = angular.module('app', ['ui.grid']);
10487
10488 app.controller('MainCtrl', ['$scope', 'gridUtil', function ($scope, gridUtil) {
10489 $scope.name = 'firstName';
10490 $scope.columnName = function(name) {
10491 return gridUtil.readableColumnName(name);
10492 };
10493 }]);
10494 </file>
10495 <file name="index.html">
10496 <div ng-controller="MainCtrl">
10497 <strong>Column name:</strong> <input ng-model="name" />
10498 <br>
10499 <strong>Output:</strong> <span ng-bind="columnName(name)"></span>
10500 </div>
10501 </file>
10502 </example>
10503 */
10504 readableColumnName: function (columnName) {
10505 // Convert underscores to spaces
10506 if (typeof(columnName) === 'undefined' || columnName === undefined || columnName === null) { return columnName; }
10507
10508 if (typeof(columnName) !== 'string') {
10509 columnName = String(columnName);
10510 }
10511
10512 return columnName.replace(/_+/g, ' ')
10513 // Replace a completely all-capsed word with a first-letter-capitalized version
10514 .replace(/^[A-Z]+$/, function (match) {
10515 return angular.lowercase(angular.uppercase(match.charAt(0)) + match.slice(1));
10516 })
10517 // Capitalize the first letter of words
10518 .replace(/([\w\u00C0-\u017F]+)/g, function (match) {
10519 return angular.uppercase(match.charAt(0)) + match.slice(1);
10520 })
10521 // Put a space in between words that have partial capilizations (i.e. 'firstName' becomes 'First Name')
10522 // .replace(/([A-Z]|[A-Z]\w+)([A-Z])/g, "$1 $2");
10523 // .replace(/(\w+?|\w)([A-Z])/g, "$1 $2");
10524 .replace(/(\w+?(?=[A-Z]))/g, '$1 ');
10525 },
10526
10527 /**
10528 * @ngdoc method
10529 * @name getColumnsFromData
10530 * @methodOf ui.grid.service:GridUtil
10531 * @description Return a list of column names, given a data set
10532 *
10533 * @param {string} data Data array for grid
10534 * @returns {Object} Column definitions with field accessor and column name
10535 *
10536 * @example
10537 <pre>
10538 var data = [
10539 { firstName: 'Bob', lastName: 'Jones' },
10540 { firstName: 'Frank', lastName: 'Smith' }
10541 ];
10542
10543 var columnDefs = GridUtil.getColumnsFromData(data, excludeProperties);
10544
10545 columnDefs == [
10546 {
10547 field: 'firstName',
10548 name: 'First Name'
10549 },
10550 {
10551 field: 'lastName',
10552 name: 'Last Name'
10553 }
10554 ];
10555 </pre>
10556 */
10557 getColumnsFromData: function (data, excludeProperties) {
10558 var columnDefs = [];
10559
10560 if (!data || typeof(data[0]) === 'undefined' || data[0] === undefined) { return []; }
10561 if (angular.isUndefined(excludeProperties)) { excludeProperties = []; }
10562
10563 var item = data[0];
10564
10565 angular.forEach(item,function (prop, propName) {
10566 if ( excludeProperties.indexOf(propName) === -1){
10567 columnDefs.push({
10568 name: propName
10569 });
10570 }
10571 });
10572
10573 return columnDefs;
10574 },
10575
10576 /**
10577 * @ngdoc method
10578 * @name newId
10579 * @methodOf ui.grid.service:GridUtil
10580 * @description Return a unique ID string
10581 *
10582 * @returns {string} Unique string
10583 *
10584 * @example
10585 <pre>
10586 var id = GridUtil.newId();
10587
10588 # 1387305700482;
10589 </pre>
10590 */
10591 newId: (function() {
10592 var seedId = new Date().getTime();
10593 return function() {
10594 return seedId += 1;
10595 };
10596 })(),
10597
10598
10599 /**
10600 * @ngdoc method
10601 * @name getTemplate
10602 * @methodOf ui.grid.service:GridUtil
10603 * @description Get's template from cache / element / url
10604 *
10605 * @param {string|element|promise} Either a string representing the template id, a string representing the template url,
10606 * an jQuery/Angualr element, or a promise that returns the template contents to use.
10607 * @returns {object} a promise resolving to template contents
10608 *
10609 * @example
10610 <pre>
10611 GridUtil.getTemplate(url).then(function (contents) {
10612 alert(contents);
10613 })
10614 </pre>
10615 */
10616 getTemplate: function (template) {
10617 // Try to fetch the template out of the templateCache
10618 if ($templateCache.get(template)) {
10619 return s.postProcessTemplate($templateCache.get(template));
10620 }
10621
10622 // See if the template is itself a promise
10623 if (template.hasOwnProperty('then')) {
10624 return template.then(s.postProcessTemplate);
10625 }
10626
10627 // If the template is an element, return the element
10628 try {
10629 if (angular.element(template).length > 0) {
10630 return $q.when(template).then(s.postProcessTemplate);
10631 }
10632 }
10633 catch (err){
10634 //do nothing; not valid html
10635 }
10636
10637 s.logDebug('fetching url', template);
10638
10639 // Default to trying to fetch the template as a url with $http
10640 return $http({ method: 'GET', url: template})
10641 .then(
10642 function (result) {
10643 var templateHtml = result.data.trim();
10644 //put in templateCache for next call
10645 $templateCache.put(template, templateHtml);
10646 return templateHtml;
10647 },
10648 function (err) {
10649 throw new Error("Could not get template " + template + ": " + err);
10650 }
10651 )
10652 .then(s.postProcessTemplate);
10653 },
10654
10655 //
10656 postProcessTemplate: function (template) {
10657 var startSym = $interpolate.startSymbol(),
10658 endSym = $interpolate.endSymbol();
10659
10660 // If either of the interpolation symbols have been changed, we need to alter this template
10661 if (startSym !== '{{' || endSym !== '}}') {
10662 template = template.replace(/\{\{/g, startSym);
10663 template = template.replace(/\}\}/g, endSym);
10664 }
10665
10666 return $q.when(template);
10667 },
10668
10669 /**
10670 * @ngdoc method
10671 * @name guessType
10672 * @methodOf ui.grid.service:GridUtil
10673 * @description guesses the type of an argument
10674 *
10675 * @param {string/number/bool/object} item variable to examine
10676 * @returns {string} one of the following
10677 * - 'string'
10678 * - 'boolean'
10679 * - 'number'
10680 * - 'date'
10681 * - 'object'
10682 */
10683 guessType : function (item) {
10684 var itemType = typeof(item);
10685
10686 // Check for numbers and booleans
10687 switch (itemType) {
10688 case "number":
10689 case "boolean":
10690 case "string":
10691 return itemType;
10692 default:
10693 if (angular.isDate(item)) {
10694 return "date";
10695 }
10696 return "object";
10697 }
10698 },
10699
10700
10701 /**
10702 * @ngdoc method
10703 * @name elementWidth
10704 * @methodOf ui.grid.service:GridUtil
10705 *
10706 * @param {element} element DOM element
10707 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10708 *
10709 * @returns {number} Element width in pixels, accounting for any borders, etc.
10710 */
10711 elementWidth: function (elem) {
10712
10713 },
10714
10715 /**
10716 * @ngdoc method
10717 * @name elementHeight
10718 * @methodOf ui.grid.service:GridUtil
10719 *
10720 * @param {element} element DOM element
10721 * @param {string} [extra] Optional modifier for calculation. Use 'margin' to account for margins on element
10722 *
10723 * @returns {number} Element height in pixels, accounting for any borders, etc.
10724 */
10725 elementHeight: function (elem) {
10726
10727 },
10728
10729 // Thanks to http://stackoverflow.com/a/13382873/888165
10730 getScrollbarWidth: function() {
10731 var outer = document.createElement("div");
10732 outer.style.visibility = "hidden";
10733 outer.style.width = "100px";
10734 outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
10735
10736 document.body.appendChild(outer);
10737
10738 var widthNoScroll = outer.offsetWidth;
10739 // force scrollbars
10740 outer.style.overflow = "scroll";
10741
10742 // add innerdiv
10743 var inner = document.createElement("div");
10744 inner.style.width = "100%";
10745 outer.appendChild(inner);
10746
10747 var widthWithScroll = inner.offsetWidth;
10748
10749 // remove divs
10750 outer.parentNode.removeChild(outer);
10751
10752 return widthNoScroll - widthWithScroll;
10753 },
10754
10755 swap: function( elem, options, callback, args ) {
10756 var ret, name,
10757 old = {};
10758
10759 // Remember the old values, and insert the new ones
10760 for ( name in options ) {
10761 old[ name ] = elem.style[ name ];
10762 elem.style[ name ] = options[ name ];
10763 }
10764
10765 ret = callback.apply( elem, args || [] );
10766
10767 // Revert the old values
10768 for ( name in options ) {
10769 elem.style[ name ] = old[ name ];
10770 }
10771
10772 return ret;
10773 },
10774
10775 fakeElement: function( elem, options, callback, args ) {
10776 var ret, name,
10777 newElement = angular.element(elem).clone()[0];
10778
10779 for ( name in options ) {
10780 newElement.style[ name ] = options[ name ];
10781 }
10782
10783 angular.element(document.body).append(newElement);
10784
10785 ret = callback.call( newElement, newElement );
10786
10787 angular.element(newElement).remove();
10788
10789 return ret;
10790 },
10791
10792 /**
10793 * @ngdoc method
10794 * @name normalizeWheelEvent
10795 * @methodOf ui.grid.service:GridUtil
10796 *
10797 * @param {event} event A mouse wheel event
10798 *
10799 * @returns {event} A normalized event
10800 *
10801 * @description
10802 * Given an event from this list:
10803 *
10804 * `wheel, mousewheel, DomMouseScroll, MozMousePixelScroll`
10805 *
10806 * "normalize" it
10807 * so that it stays consistent no matter what browser it comes from (i.e. scale it correctly and make sure the direction is right.)
10808 */
10809 normalizeWheelEvent: function (event) {
10810 // var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'];
10811 // var toBind = 'onwheel' in document || document.documentMode >= 9 ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'];
10812 var lowestDelta, lowestDeltaXY;
10813
10814 var orgEvent = event || window.event,
10815 args = [].slice.call(arguments, 1),
10816 delta = 0,
10817 deltaX = 0,
10818 deltaY = 0,
10819 absDelta = 0,
10820 absDeltaXY = 0,
10821 fn;
10822
10823 // event = $.event.fix(orgEvent);
10824 // event.type = 'mousewheel';
10825
10826 // NOTE: jQuery masks the event and stores it in the event as originalEvent
10827 if (orgEvent.originalEvent) {
10828 orgEvent = orgEvent.originalEvent;
10829 }
10830
10831 // Old school scrollwheel delta
10832 if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta; }
10833 if ( orgEvent.detail ) { delta = orgEvent.detail * -1; }
10834
10835 // At a minimum, setup the deltaY to be delta
10836 deltaY = delta;
10837
10838 // Firefox < 17 related to DOMMouseScroll event
10839 if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) {
10840 deltaY = 0;
10841 deltaX = delta * -1;
10842 }
10843
10844 // New school wheel delta (wheel event)
10845 if ( orgEvent.deltaY ) {
10846 deltaY = orgEvent.deltaY * -1;
10847 delta = deltaY;
10848 }
10849 if ( orgEvent.deltaX ) {
10850 deltaX = orgEvent.deltaX;
10851 delta = deltaX * -1;
10852 }
10853
10854 // Webkit
10855 if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY; }
10856 if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = orgEvent.wheelDeltaX; }
10857
10858 // Look for lowest delta to normalize the delta values
10859 absDelta = Math.abs(delta);
10860 if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; }
10861 absDeltaXY = Math.max(Math.abs(deltaY), Math.abs(deltaX));
10862 if ( !lowestDeltaXY || absDeltaXY < lowestDeltaXY ) { lowestDeltaXY = absDeltaXY; }
10863
10864 // Get a whole value for the deltas
10865 fn = delta > 0 ? 'floor' : 'ceil';
10866 delta = Math[fn](delta / lowestDelta);
10867 deltaX = Math[fn](deltaX / lowestDeltaXY);
10868 deltaY = Math[fn](deltaY / lowestDeltaXY);
10869
10870 return {
10871 delta: delta,
10872 deltaX: deltaX,
10873 deltaY: deltaY
10874 };
10875 },
10876
10877 // Stolen from Modernizr
10878 // TODO: make this, and everythign that flows from it, robust
10879 //http://www.stucox.com/blog/you-cant-detect-a-touchscreen/
10880 isTouchEnabled: function() {
10881 var bool;
10882
10883 if (('ontouchstart' in $window) || $window.DocumentTouch && $document instanceof DocumentTouch) {
10884 bool = true;
10885 }
10886
10887 return bool;
10888 },
10889
10890 isNullOrUndefined: function(obj) {
10891 if (obj === undefined || obj === null) {
10892 return true;
10893 }
10894 return false;
10895 },
10896
10897 endsWith: function(str, suffix) {
10898 if (!str || !suffix || typeof str !== "string") {
10899 return false;
10900 }
10901 return str.indexOf(suffix, str.length - suffix.length) !== -1;
10902 },
10903
10904 arrayContainsObjectWithProperty: function(array, propertyName, propertyValue) {
10905 var found = false;
10906 angular.forEach(array, function (object) {
10907 if (object[propertyName] === propertyValue) {
10908 found = true;
10909 }
10910 });
10911 return found;
10912 },
10913
10914 //// Shim requestAnimationFrame
10915 //requestAnimationFrame: $window.requestAnimationFrame && $window.requestAnimationFrame.bind($window) ||
10916 // $window.webkitRequestAnimationFrame && $window.webkitRequestAnimationFrame.bind($window) ||
10917 // function(fn) {
10918 // return $timeout(fn, 10, false);
10919 // },
10920
10921 numericAndNullSort: function (a, b) {
10922 if (a === null) { return 1; }
10923 if (b === null) { return -1; }
10924 if (a === null && b === null) { return 0; }
10925 return a - b;
10926 },
10927
10928 // Disable ngAnimate animations on an element
10929 disableAnimations: function (element) {
10930 var $animate;
10931 try {
10932 $animate = $injector.get('$animate');
10933 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10934 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10935 $animate.enabled(element, false);
10936 } else {
10937 $animate.enabled(false, element);
10938 }
10939 }
10940 catch (e) {}
10941 },
10942
10943 enableAnimations: function (element) {
10944 var $animate;
10945 try {
10946 $animate = $injector.get('$animate');
10947 // See: http://brianhann.com/angular-1-4-breaking-changes-to-be-aware-of/#animate
10948 if (angular.version.major > 1 || (angular.version.major === 1 && angular.version.minor >= 4)) {
10949 $animate.enabled(element, true);
10950 } else {
10951 $animate.enabled(true, element);
10952 }
10953 return $animate;
10954 }
10955 catch (e) {}
10956 },
10957
10958 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
10959 nextUid: function nextUid() {
10960 var index = uid.length;
10961 var digit;
10962
10963 while (index) {
10964 index--;
10965 digit = uid[index].charCodeAt(0);
10966 if (digit === 57 /*'9'*/) {
10967 uid[index] = 'A';
10968 return uidPrefix + uid.join('');
10969 }
10970 if (digit === 90 /*'Z'*/) {
10971 uid[index] = '0';
10972 } else {
10973 uid[index] = String.fromCharCode(digit + 1);
10974 return uidPrefix + uid.join('');
10975 }
10976 }
10977 uid.unshift('0');
10978
10979 return uidPrefix + uid.join('');
10980 },
10981
10982 // Blatantly stolen from Angular as it isn't exposed (yet. 2.0 maybe?)
10983 hashKey: function hashKey(obj) {
10984 var objType = typeof obj,
10985 key;
10986
10987 if (objType === 'object' && obj !== null) {
10988 if (typeof (key = obj.$$hashKey) === 'function') {
10989 // must invoke on object to keep the right this
10990 key = obj.$$hashKey();
10991 }
10992 else if (typeof(obj.$$hashKey) !== 'undefined' && obj.$$hashKey) {
10993 key = obj.$$hashKey;
10994 }
10995 else if (key === undefined) {
10996 key = obj.$$hashKey = s.nextUid();
10997 }
10998 }
10999 else {
11000 key = obj;
11001 }
11002
11003 return objType + ':' + key;
11004 },
11005
11006 resetUids: function () {
11007 uid = ['0', '0', '0'];
11008 },
11009
11010 /**
11011 * @ngdoc method
11012 * @methodOf ui.grid.service:GridUtil
11013 * @name logError
11014 * @description wraps the $log method, allowing us to choose different
11015 * treatment within ui-grid if we so desired. At present we only log
11016 * error messages if uiGridConstants.LOG_ERROR_MESSAGES is set to true
11017 * @param {string} logMessage message to be logged to the console
11018 *
11019 */
11020 logError: function( logMessage ){
11021 if ( uiGridConstants.LOG_ERROR_MESSAGES ){
11022 $log.error( logMessage );
11023 }
11024 },
11025
11026 /**
11027 * @ngdoc method
11028 * @methodOf ui.grid.service:GridUtil
11029 * @name logWarn
11030 * @description wraps the $log method, allowing us to choose different
11031 * treatment within ui-grid if we so desired. At present we only log
11032 * warning messages if uiGridConstants.LOG_WARN_MESSAGES is set to true
11033 * @param {string} logMessage message to be logged to the console
11034 *
11035 */
11036 logWarn: function( logMessage ){
11037 if ( uiGridConstants.LOG_WARN_MESSAGES ){
11038 $log.warn( logMessage );
11039 }
11040 },
11041
11042 /**
11043 * @ngdoc method
11044 * @methodOf ui.grid.service:GridUtil
11045 * @name logDebug
11046 * @description wraps the $log method, allowing us to choose different
11047 * treatment within ui-grid if we so desired. At present we only log
11048 * debug messages if uiGridConstants.LOG_DEBUG_MESSAGES is set to true
11049 *
11050 */
11051 logDebug: function() {
11052 if ( uiGridConstants.LOG_DEBUG_MESSAGES ){
11053 $log.debug.apply($log, arguments);
11054 }
11055 }
11056
11057 };
11058
11059 /**
11060 * @ngdoc object
11061 * @name focus
11062 * @propertyOf ui.grid.service:GridUtil
11063 * @description Provies a set of methods to set the document focus inside the grid.
11064 * See {@link ui.grid.service:GridUtil.focus} for more information.
11065 */
11066
11067 /**
11068 * @ngdoc object
11069 * @name ui.grid.service:GridUtil.focus
11070 * @description Provies a set of methods to set the document focus inside the grid.
11071 * Timeouts are utilized to ensure that the focus is invoked after any other event has been triggered.
11072 * e.g. click events that need to run before the focus or
11073 * inputs elements that are in a disabled state but are enabled when those events
11074 * are triggered.
11075 */
11076 s.focus = {
11077 queue: [],
11078 //http://stackoverflow.com/questions/25596399/set-element-focus-in-angular-way
11079 /**
11080 * @ngdoc method
11081 * @methodOf ui.grid.service:GridUtil.focus
11082 * @name byId
11083 * @description Sets the focus of the document to the given id value.
11084 * If provided with the grid object it will automatically append the grid id.
11085 * This is done to encourage unique dom id's as it allows for multiple grids on a
11086 * page.
11087 * @param {String} id the id of the dom element to set the focus on
11088 * @param {Object=} Grid the grid object for this grid instance. See: {@link ui.grid.class:Grid}
11089 * @param {Number} Grid.id the unique id for this grid. Already set on an initialized grid object.
11090 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11091 * then the promise will fail with the `'canceled'` reason.
11092 */
11093 byId: function (id, Grid) {
11094 this._purgeQueue();
11095 var promise = $timeout(function() {
11096 var elementID = (Grid && Grid.id ? Grid.id + '-' : '') + id;
11097 var element = $window.document.getElementById(elementID);
11098 if (element) {
11099 element.focus();
11100 } else {
11101 s.logWarn('[focus.byId] Element id ' + elementID + ' was not found.');
11102 }
11103 });
11104 this.queue.push(promise);
11105 return promise;
11106 },
11107
11108 /**
11109 * @ngdoc method
11110 * @methodOf ui.grid.service:GridUtil.focus
11111 * @name byElement
11112 * @description Sets the focus of the document to the given dom element.
11113 * @param {(element|angular.element)} element the DOM element to set the focus on
11114 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11115 * then the promise will fail with the `'canceled'` reason.
11116 */
11117 byElement: function(element){
11118 if (!angular.isElement(element)){
11119 s.logWarn("Trying to focus on an element that isn\'t an element.");
11120 return $q.reject('not-element');
11121 }
11122 element = angular.element(element);
11123 this._purgeQueue();
11124 var promise = $timeout(function(){
11125 if (element){
11126 element[0].focus();
11127 }
11128 });
11129 this.queue.push(promise);
11130 return promise;
11131 },
11132 /**
11133 * @ngdoc method
11134 * @methodOf ui.grid.service:GridUtil.focus
11135 * @name bySelector
11136 * @description Sets the focus of the document to the given dom element.
11137 * @param {(element|angular.element)} parentElement the parent/ancestor of the dom element that you are selecting using the query selector
11138 * @param {String} querySelector finds the dom element using the {@link http://www.w3schools.com/jsref/met_document_queryselector.asp querySelector}
11139 * @param {boolean} [aSync=false] If true then the selector will be querried inside of a timeout. Otherwise the selector will be querried imidately
11140 * then the focus will be called.
11141 * @returns {Promise} The `$timeout` promise that will be resolved once focus is set. If another focus is requested before this request is evaluated.
11142 * then the promise will fail with the `'canceled'` reason.
11143 */
11144 bySelector: function(parentElement, querySelector, aSync){
11145 var self = this;
11146 if (!angular.isElement(parentElement)){
11147 throw new Error("The parent element is not an element.");
11148 }
11149 // Ensure that this is an angular element.
11150 // It is fine if this is already an angular element.
11151 parentElement = angular.element(parentElement);
11152 var focusBySelector = function(){
11153 var element = parentElement[0].querySelector(querySelector);
11154 return self.byElement(element);
11155 };
11156 this._purgeQueue();
11157 if (aSync){ //Do this asynchronysly
11158 var promise = $timeout(focusBySelector);
11159 this.queue.push($timeout(focusBySelector));
11160 return promise;
11161 } else {
11162 return focusBySelector();
11163 }
11164 },
11165 _purgeQueue: function(){
11166 this.queue.forEach(function(element){
11167 $timeout.cancel(element);
11168 });
11169 this.queue = [];
11170 }
11171 };
11172
11173
11174 ['width', 'height'].forEach(function (name) {
11175 var capsName = angular.uppercase(name.charAt(0)) + name.substr(1);
11176 s['element' + capsName] = function (elem, extra) {
11177 var e = elem;
11178 if (e && typeof(e.length) !== 'undefined' && e.length) {
11179 e = elem[0];
11180 }
11181
11182 if (e) {
11183 var styles = getStyles(e);
11184 return e.offsetWidth === 0 && rdisplayswap.test(styles.display) ?
11185 s.swap(e, cssShow, function() {
11186 return getWidthOrHeight(e, name, extra );
11187 }) :
11188 getWidthOrHeight( e, name, extra );
11189 }
11190 else {
11191 return null;
11192 }
11193 };
11194
11195 s['outerElement' + capsName] = function (elem, margin) {
11196 return elem ? s['element' + capsName].call(this, elem, margin ? 'margin' : 'border') : null;
11197 };
11198 });
11199
11200 // http://stackoverflow.com/a/24107550/888165
11201 s.closestElm = function closestElm(el, selector) {
11202 if (typeof(el.length) !== 'undefined' && el.length) {
11203 el = el[0];
11204 }
11205
11206 var matchesFn;
11207
11208 // find vendor prefix
11209 ['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) {
11210 if (typeof document.body[fn] === 'function') {
11211 matchesFn = fn;
11212 return true;
11213 }
11214 return false;
11215 });
11216
11217 // traverse parents
11218 var parent;
11219 while (el !== null) {
11220 parent = el.parentElement;
11221 if (parent !== null && parent[matchesFn](selector)) {
11222 return parent;
11223 }
11224 el = parent;
11225 }
11226
11227 return null;
11228 };
11229
11230 s.type = function (obj) {
11231 var text = Function.prototype.toString.call(obj.constructor);
11232 return text.match(/function (.*?)\(/)[1];
11233 };
11234
11235 s.getBorderSize = function getBorderSize(elem, borderType) {
11236 if (typeof(elem.length) !== 'undefined' && elem.length) {
11237 elem = elem[0];
11238 }
11239
11240 var styles = getStyles(elem);
11241
11242 // If a specific border is supplied, like 'top', read the 'borderTop' style property
11243 if (borderType) {
11244 borderType = 'border' + borderType.charAt(0).toUpperCase() + borderType.slice(1);
11245 }
11246 else {
11247 borderType = 'border';
11248 }
11249
11250 borderType += 'Width';
11251
11252 var val = parseInt(styles[borderType], 10);
11253
11254 if (isNaN(val)) {
11255 return 0;
11256 }
11257 else {
11258 return val;
11259 }
11260 };
11261
11262 // http://stackoverflow.com/a/22948274/888165
11263 // TODO: Opera? Mobile?
11264 s.detectBrowser = function detectBrowser() {
11265 var userAgent = $window.navigator.userAgent;
11266
11267 var browsers = {chrome: /chrome/i, safari: /safari/i, firefox: /firefox/i, ie: /internet explorer|trident\//i};
11268
11269 for (var key in browsers) {
11270 if (browsers[key].test(userAgent)) {
11271 return key;
11272 }
11273 }
11274
11275 return 'unknown';
11276 };
11277
11278 // Borrowed from https://github.com/othree/jquery.rtl-scroll-type
11279 // Determine the scroll "type" this browser is using for RTL
11280 s.rtlScrollType = function rtlScrollType() {
11281 if (rtlScrollType.type) {
11282 return rtlScrollType.type;
11283 }
11284
11285 var definer = angular.element('<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>')[0],
11286 type = 'reverse';
11287
11288 document.body.appendChild(definer);
11289
11290 if (definer.scrollLeft > 0) {
11291 type = 'default';
11292 }
11293 else {
11294 definer.scrollLeft = 1;
11295 if (definer.scrollLeft === 0) {
11296 type = 'negative';
11297 }
11298 }
11299
11300 angular.element(definer).remove();
11301 rtlScrollType.type = type;
11302
11303 return type;
11304 };
11305
11306 /**
11307 * @ngdoc method
11308 * @name normalizeScrollLeft
11309 * @methodOf ui.grid.service:GridUtil
11310 *
11311 * @param {element} element The element to get the `scrollLeft` from.
11312 * @param {grid} grid - grid used to normalize (uses the rtl property)
11313 *
11314 * @returns {number} A normalized scrollLeft value for the current browser.
11315 *
11316 * @description
11317 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method normalizes them
11318 */
11319 s.normalizeScrollLeft = function normalizeScrollLeft(element, grid) {
11320 if (typeof(element.length) !== 'undefined' && element.length) {
11321 element = element[0];
11322 }
11323
11324 var scrollLeft = element.scrollLeft;
11325
11326 if (grid.isRTL()) {
11327 switch (s.rtlScrollType()) {
11328 case 'default':
11329 return element.scrollWidth - scrollLeft - element.clientWidth;
11330 case 'negative':
11331 return Math.abs(scrollLeft);
11332 case 'reverse':
11333 return scrollLeft;
11334 }
11335 }
11336
11337 return scrollLeft;
11338 };
11339
11340 /**
11341 * @ngdoc method
11342 * @name denormalizeScrollLeft
11343 * @methodOf ui.grid.service:GridUtil
11344 *
11345 * @param {element} element The element to normalize the `scrollLeft` value for
11346 * @param {number} scrollLeft The `scrollLeft` value to denormalize.
11347 * @param {grid} grid The grid that owns the scroll event.
11348 *
11349 * @returns {number} A normalized scrollLeft value for the current browser.
11350 *
11351 * @description
11352 * Browsers currently handle RTL in different ways, resulting in inconsistent scrollLeft values. This method denormalizes a value for the current browser.
11353 */
11354 s.denormalizeScrollLeft = function denormalizeScrollLeft(element, scrollLeft, grid) {
11355 if (typeof(element.length) !== 'undefined' && element.length) {
11356 element = element[0];
11357 }
11358
11359 if (grid.isRTL()) {
11360 switch (s.rtlScrollType()) {
11361 case 'default':
11362 // Get the max scroll for the element
11363 var maxScrollLeft = element.scrollWidth - element.clientWidth;
11364
11365 // Subtract the current scroll amount from the max scroll
11366 return maxScrollLeft - scrollLeft;
11367 case 'negative':
11368 return scrollLeft * -1;
11369 case 'reverse':
11370 return scrollLeft;
11371 }
11372 }
11373
11374 return scrollLeft;
11375 };
11376
11377 /**
11378 * @ngdoc method
11379 * @name preEval
11380 * @methodOf ui.grid.service:GridUtil
11381 *
11382 * @param {string} path Path to evaluate
11383 *
11384 * @returns {string} A path that is normalized.
11385 *
11386 * @description
11387 * Takes a field path and converts it to bracket notation to allow for special characters in path
11388 * @example
11389 * <pre>
11390 * gridUtil.preEval('property') == 'property'
11391 * gridUtil.preEval('nested.deep.prop-erty') = "nested['deep']['prop-erty']"
11392 * </pre>
11393 */
11394 s.preEval = function (path) {
11395 var m = uiGridConstants.BRACKET_REGEXP.exec(path);
11396 if (m) {
11397 return (m[1] ? s.preEval(m[1]) : m[1]) + m[2] + (m[3] ? s.preEval(m[3]) : m[3]);
11398 } else {
11399 path = path.replace(uiGridConstants.APOS_REGEXP, '\\\'');
11400 var parts = path.split(uiGridConstants.DOT_REGEXP);
11401 var preparsed = [parts.shift()]; // first item must be var notation, thus skip
11402 angular.forEach(parts, function (part) {
11403 preparsed.push(part.replace(uiGridConstants.FUNC_REGEXP, '\']$1'));
11404 });
11405 return preparsed.join('[\'');
11406 }
11407 };
11408
11409 /**
11410 * @ngdoc method
11411 * @name debounce
11412 * @methodOf ui.grid.service:GridUtil
11413 *
11414 * @param {function} func function to debounce
11415 * @param {number} wait milliseconds to delay
11416 * @param {boolean} immediate execute before delay
11417 *
11418 * @returns {function} A function that can be executed as debounced function
11419 *
11420 * @description
11421 * Copied from https://github.com/shahata/angular-debounce
11422 * Takes a function, decorates it to execute only 1 time after multiple calls, and returns the decorated function
11423 * @example
11424 * <pre>
11425 * var debouncedFunc = gridUtil.debounce(function(){alert('debounced');}, 500);
11426 * debouncedFunc();
11427 * debouncedFunc();
11428 * debouncedFunc();
11429 * </pre>
11430 */
11431 s.debounce = function (func, wait, immediate) {
11432 var timeout, args, context, result;
11433 function debounce() {
11434 /* jshint validthis:true */
11435 context = this;
11436 args = arguments;
11437 var later = function () {
11438 timeout = null;
11439 if (!immediate) {
11440 result = func.apply(context, args);
11441 }
11442 };
11443 var callNow = immediate && !timeout;
11444 if (timeout) {
11445 $timeout.cancel(timeout);
11446 }
11447 timeout = $timeout(later, wait);
11448 if (callNow) {
11449 result = func.apply(context, args);
11450 }
11451 return result;
11452 }
11453 debounce.cancel = function () {
11454 $timeout.cancel(timeout);
11455 timeout = null;
11456 };
11457 return debounce;
11458 };
11459
11460 /**
11461 * @ngdoc method
11462 * @name throttle
11463 * @methodOf ui.grid.service:GridUtil
11464 *
11465 * @param {function} func function to throttle
11466 * @param {number} wait milliseconds to delay after first trigger
11467 * @param {Object} params to use in throttle.
11468 *
11469 * @returns {function} A function that can be executed as throttled function
11470 *
11471 * @description
11472 * Adapted from debounce function (above)
11473 * Potential keys for Params Object are:
11474 * trailing (bool) - whether to trigger after throttle time ends if called multiple times
11475 * Updated to use $interval rather than $timeout, as protractor (e2e tests) is able to work with $interval,
11476 * but not with $timeout
11477 *
11478 * Note that when using throttle, you need to use throttle to create a new function upfront, then use the function
11479 * return from that call each time you need to call throttle. If you call throttle itself repeatedly, the lastCall
11480 * variable will get overwritten and the throttling won't work
11481 *
11482 * @example
11483 * <pre>
11484 * var throttledFunc = gridUtil.throttle(function(){console.log('throttled');}, 500, {trailing: true});
11485 * throttledFunc(); //=> logs throttled
11486 * throttledFunc(); //=> queues attempt to log throttled for ~500ms (since trailing param is truthy)
11487 * throttledFunc(); //=> updates arguments to keep most-recent request, but does not do anything else.
11488 * </pre>
11489 */
11490 s.throttle = function(func, wait, options){
11491 options = options || {};
11492 var lastCall = 0, queued = null, context, args;
11493
11494 function runFunc(endDate){
11495 lastCall = +new Date();
11496 func.apply(context, args);
11497 $interval(function(){ queued = null; }, 0, 1);
11498 }
11499
11500 return function(){
11501 /* jshint validthis:true */
11502 context = this;
11503 args = arguments;
11504 if (queued === null){
11505 var sinceLast = +new Date() - lastCall;
11506 if (sinceLast > wait){
11507 runFunc();
11508 }
11509 else if (options.trailing){
11510 queued = $interval(runFunc, wait - sinceLast, 1);
11511 }
11512 }
11513 };
11514 };
11515
11516 s.on = {};
11517 s.off = {};
11518 s._events = {};
11519
11520 s.addOff = function (eventName) {
11521 s.off[eventName] = function (elm, fn) {
11522 var idx = s._events[eventName].indexOf(fn);
11523 if (idx > 0) {
11524 s._events[eventName].removeAt(idx);
11525 }
11526 };
11527 };
11528
11529 var mouseWheeltoBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'],
11530 nullLowestDeltaTimeout,
11531 lowestDelta;
11532
11533 s.on.mousewheel = function (elm, fn) {
11534 if (!elm || !fn) { return; }
11535
11536 var $elm = angular.element(elm);
11537
11538 // Store the line height and page height for this particular element
11539 $elm.data('mousewheel-line-height', getLineHeight($elm));
11540 $elm.data('mousewheel-page-height', s.elementHeight($elm));
11541 if (!$elm.data('mousewheel-callbacks')) { $elm.data('mousewheel-callbacks', {}); }
11542
11543 var cbs = $elm.data('mousewheel-callbacks');
11544 cbs[fn] = (Function.prototype.bind || bindPolyfill).call(mousewheelHandler, $elm[0], fn);
11545
11546 // Bind all the mousew heel events
11547 for ( var i = mouseWheeltoBind.length; i; ) {
11548 $elm.on(mouseWheeltoBind[--i], cbs[fn]);
11549 }
11550 };
11551 s.off.mousewheel = function (elm, fn) {
11552 var $elm = angular.element(elm);
11553
11554 var cbs = $elm.data('mousewheel-callbacks');
11555 var handler = cbs[fn];
11556
11557 if (handler) {
11558 for ( var i = mouseWheeltoBind.length; i; ) {
11559 $elm.off(mouseWheeltoBind[--i], handler);
11560 }
11561 }
11562
11563 delete cbs[fn];
11564
11565 if (Object.keys(cbs).length === 0) {
11566 $elm.removeData('mousewheel-line-height');
11567 $elm.removeData('mousewheel-page-height');
11568 $elm.removeData('mousewheel-callbacks');
11569 }
11570 };
11571
11572 function mousewheelHandler(fn, event) {
11573 var $elm = angular.element(this);
11574
11575 var delta = 0,
11576 deltaX = 0,
11577 deltaY = 0,
11578 absDelta = 0,
11579 offsetX = 0,
11580 offsetY = 0;
11581
11582 // jQuery masks events
11583 if (event.originalEvent) { event = event.originalEvent; }
11584
11585 if ( 'detail' in event ) { deltaY = event.detail * -1; }
11586 if ( 'wheelDelta' in event ) { deltaY = event.wheelDelta; }
11587 if ( 'wheelDeltaY' in event ) { deltaY = event.wheelDeltaY; }
11588 if ( 'wheelDeltaX' in event ) { deltaX = event.wheelDeltaX * -1; }
11589
11590 // Firefox < 17 horizontal scrolling related to DOMMouseScroll event
11591 if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
11592 deltaX = deltaY * -1;
11593 deltaY = 0;
11594 }
11595
11596 // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy
11597 delta = deltaY === 0 ? deltaX : deltaY;
11598
11599 // New school wheel delta (wheel event)
11600 if ( 'deltaY' in event ) {
11601 deltaY = event.deltaY * -1;
11602 delta = deltaY;
11603 }
11604 if ( 'deltaX' in event ) {
11605 deltaX = event.deltaX;
11606 if ( deltaY === 0 ) { delta = deltaX * -1; }
11607 }
11608
11609 // No change actually happened, no reason to go any further
11610 if ( deltaY === 0 && deltaX === 0 ) { return; }
11611
11612 // Need to convert lines and pages to pixels if we aren't already in pixels
11613 // There are three delta modes:
11614 // * deltaMode 0 is by pixels, nothing to do
11615 // * deltaMode 1 is by lines
11616 // * deltaMode 2 is by pages
11617 if ( event.deltaMode === 1 ) {
11618 var lineHeight = $elm.data('mousewheel-line-height');
11619 delta *= lineHeight;
11620 deltaY *= lineHeight;
11621 deltaX *= lineHeight;
11622 }
11623 else if ( event.deltaMode === 2 ) {
11624 var pageHeight = $elm.data('mousewheel-page-height');
11625 delta *= pageHeight;
11626 deltaY *= pageHeight;
11627 deltaX *= pageHeight;
11628 }
11629
11630 // Store lowest absolute delta to normalize the delta values
11631 absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) );
11632
11633 if ( !lowestDelta || absDelta < lowestDelta ) {
11634 lowestDelta = absDelta;
11635
11636 // Adjust older deltas if necessary
11637 if ( shouldAdjustOldDeltas(event, absDelta) ) {
11638 lowestDelta /= 40;
11639 }
11640 }
11641
11642 // Get a whole, normalized value for the deltas
11643 delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta);
11644 deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta);
11645 deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta);
11646
11647 event.deltaMode = 0;
11648
11649 // Normalise offsetX and offsetY properties
11650 // if ($elm[0].getBoundingClientRect ) {
11651 // var boundingRect = $(elm)[0].getBoundingClientRect();
11652 // offsetX = event.clientX - boundingRect.left;
11653 // offsetY = event.clientY - boundingRect.top;
11654 // }
11655
11656 // event.deltaX = deltaX;
11657 // event.deltaY = deltaY;
11658 // event.deltaFactor = lowestDelta;
11659
11660 var newEvent = {
11661 originalEvent: event,
11662 deltaX: deltaX,
11663 deltaY: deltaY,
11664 deltaFactor: lowestDelta,
11665 preventDefault: function () { event.preventDefault(); },
11666 stopPropagation: function () { event.stopPropagation(); }
11667 };
11668
11669 // Clearout lowestDelta after sometime to better
11670 // handle multiple device types that give
11671 // a different lowestDelta
11672 // Ex: trackpad = 3 and mouse wheel = 120
11673 if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); }
11674 nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200);
11675
11676 fn.call($elm[0], newEvent);
11677 }
11678
11679 function nullLowestDelta() {
11680 lowestDelta = null;
11681 }
11682
11683 function shouldAdjustOldDeltas(orgEvent, absDelta) {
11684 // If this is an older event and the delta is divisable by 120,
11685 // then we are assuming that the browser is treating this as an
11686 // older mouse wheel event and that we should divide the deltas
11687 // by 40 to try and get a more usable deltaFactor.
11688 // Side note, this actually impacts the reported scroll distance
11689 // in older browsers and can cause scrolling to be slower than native.
11690 // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false.
11691 return orgEvent.type === 'mousewheel' && absDelta % 120 === 0;
11692 }
11693
11694 return s;
11695 }]);
11696
11697 // Add 'px' to the end of a number string if it doesn't have it already
11698 module.filter('px', function() {
11699 return function(str) {
11700 if (str.match(/^[\d\.]+$/)) {
11701 return str + 'px';
11702 }
11703 else {
11704 return str;
11705 }
11706 };
11707 });
11708
11709 })();
11710
11711 (function () {
11712 angular.module('ui.grid').config(['$provide', function($provide) {
11713 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11714 var lang = {
11715 aggregate: {
11716 label: 'položky'
11717 },
11718 groupPanel: {
11719 description: 'Přesuňte záhlaví zde pro vytvoření skupiny dle sloupce.'
11720 },
11721 search: {
11722 placeholder: 'Hledat...',
11723 showingItems: 'Zobrazuji položky:',
11724 selectedItems: 'Vybrané položky:',
11725 totalItems: 'Celkem položek:',
11726 size: 'Velikost strany:',
11727 first: 'První strana',
11728 next: 'Další strana',
11729 previous: 'Předchozí strana',
11730 last: 'Poslední strana'
11731 },
11732 menu: {
11733 text: 'Vyberte sloupec:'
11734 },
11735 sort: {
11736 ascending: 'Seřadit od A-Z',
11737 descending: 'Seřadit od Z-A',
11738 remove: 'Odebrat seřazení'
11739 },
11740 column: {
11741 hide: 'Schovat sloupec'
11742 },
11743 aggregation: {
11744 count: 'celkem řádků: ',
11745 sum: 'celkem: ',
11746 avg: 'avg: ',
11747 min: 'min.: ',
11748 max: 'max.: '
11749 },
11750 pinning: {
11751 pinLeft: 'Zamknout vlevo',
11752 pinRight: 'Zamknout vpravo',
11753 unpin: 'Odemknout'
11754 },
11755 gridMenu: {
11756 columns: 'Sloupce:',
11757 importerTitle: 'Importovat soubor',
11758 exporterAllAsCsv: 'Exportovat všechna data do csv',
11759 exporterVisibleAsCsv: 'Exportovat viditelná data do csv',
11760 exporterSelectedAsCsv: 'Exportovat vybraná data do csv',
11761 exporterAllAsPdf: 'Exportovat všechna data do pdf',
11762 exporterVisibleAsPdf: 'Exportovat viditelná data do pdf',
11763 exporterSelectedAsPdf: 'Exportovat vybraná data do pdf',
11764 clearAllFilters: 'Odstranit všechny filtry'
11765 },
11766 importer: {
11767 noHeaders: 'Názvy sloupců se nepodařilo získat, obsahuje soubor záhlaví?',
11768 noObjects: 'Data se nepodařilo zpracovat, obsahuje soubor řádky mimo záhlaví?',
11769 invalidCsv: 'Soubor nelze zpracovat, jedná se o CSV?',
11770 invalidJson: 'Soubor nelze zpracovat, je to JSON?',
11771 jsonNotArray: 'Soubor musí obsahovat json. Ukončuji..'
11772 },
11773 pagination: {
11774 sizes: 'položek na stránku',
11775 totalItems: 'položek'
11776 },
11777 grouping: {
11778 group: 'Seskupit',
11779 ungroup: 'Odebrat seskupení',
11780 aggregate_count: 'Agregace: Count',
11781 aggregate_sum: 'Agregace: Sum',
11782 aggregate_max: 'Agregace: Max',
11783 aggregate_min: 'Agregace: Min',
11784 aggregate_avg: 'Agregace: Avg',
11785 aggregate_remove: 'Agregace: Odebrat'
11786 }
11787 };
11788
11789 // support varianty of different czech keys.
11790 $delegate.add('cs', lang);
11791 $delegate.add('cz', lang);
11792 $delegate.add('cs-cz', lang);
11793 $delegate.add('cs-CZ', lang);
11794 return $delegate;
11795 }]);
11796 }]);
11797 })();
11798
11799 (function(){
11800 angular.module('ui.grid').config(['$provide', function($provide) {
11801 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11802 $delegate.add('da', {
11803 aggregate:{
11804 label: 'artikler'
11805 },
11806 groupPanel:{
11807 description: 'Grupér rækker udfra en kolonne ved at trække dens overskift hertil.'
11808 },
11809 search:{
11810 placeholder: 'Søg...',
11811 showingItems: 'Viste rækker:',
11812 selectedItems: 'Valgte rækker:',
11813 totalItems: 'Rækker totalt:',
11814 size: 'Side størrelse:',
11815 first: 'Første side',
11816 next: 'Næste side',
11817 previous: 'Forrige side',
11818 last: 'Sidste side'
11819 },
11820 menu:{
11821 text: 'Vælg kolonner:'
11822 },
11823 column: {
11824 hide: 'Skjul kolonne'
11825 },
11826 aggregation: {
11827 count: 'samlede rækker: ',
11828 sum: 'smalede: ',
11829 avg: 'gns: ',
11830 min: 'min: ',
11831 max: 'max: '
11832 },
11833 gridMenu: {
11834 columns: 'Columns:',
11835 importerTitle: 'Import file',
11836 exporterAllAsCsv: 'Export all data as csv',
11837 exporterVisibleAsCsv: 'Export visible data as csv',
11838 exporterSelectedAsCsv: 'Export selected data as csv',
11839 exporterAllAsPdf: 'Export all data as pdf',
11840 exporterVisibleAsPdf: 'Export visible data as pdf',
11841 exporterSelectedAsPdf: 'Export selected data as pdf',
11842 clearAllFilters: 'Clear all filters'
11843 },
11844 importer: {
11845 noHeaders: 'Column names were unable to be derived, does the file have a header?',
11846 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
11847 invalidCsv: 'File was unable to be processed, is it valid CSV?',
11848 invalidJson: 'File was unable to be processed, is it valid Json?',
11849 jsonNotArray: 'Imported json file must contain an array, aborting.'
11850 }
11851 });
11852 return $delegate;
11853 }]);
11854 }]);
11855 })();
11856
11857 (function () {
11858 angular.module('ui.grid').config(['$provide', function ($provide) {
11859 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
11860 $delegate.add('de', {
11861 aggregate: {
11862 label: 'Eintrag'
11863 },
11864 groupPanel: {
11865 description: 'Ziehen Sie eine Spaltenüberschrift hierhin, um nach dieser Spalte zu gruppieren.'
11866 },
11867 search: {
11868 placeholder: 'Suche...',
11869 showingItems: 'Zeige Einträge:',
11870 selectedItems: 'Ausgewählte Einträge:',
11871 totalItems: 'Einträge gesamt:',
11872 size: 'Einträge pro Seite:',
11873 first: 'Erste Seite',
11874 next: 'Nächste Seite',
11875 previous: 'Vorherige Seite',
11876 last: 'Letzte Seite'
11877 },
11878 menu: {
11879 text: 'Spalten auswählen:'
11880 },
11881 sort: {
11882 ascending: 'aufsteigend sortieren',
11883 descending: 'absteigend sortieren',
11884 remove: 'Sortierung zurücksetzen'
11885 },
11886 column: {
11887 hide: 'Spalte ausblenden'
11888 },
11889 aggregation: {
11890 count: 'Zeilen insgesamt: ',
11891 sum: 'gesamt: ',
11892 avg: 'Durchschnitt: ',
11893 min: 'min: ',
11894 max: 'max: '
11895 },
11896 pinning: {
11897 pinLeft: 'Links anheften',
11898 pinRight: 'Rechts anheften',
11899 unpin: 'Lösen'
11900 },
11901 gridMenu: {
11902 columns: 'Spalten:',
11903 importerTitle: 'Datei importieren',
11904 exporterAllAsCsv: 'Alle Daten als CSV exportieren',
11905 exporterVisibleAsCsv: 'sichtbare Daten als CSV exportieren',
11906 exporterSelectedAsCsv: 'markierte Daten als CSV exportieren',
11907 exporterAllAsPdf: 'Alle Daten als PDF exportieren',
11908 exporterVisibleAsPdf: 'sichtbare Daten als PDF exportieren',
11909 exporterSelectedAsPdf: 'markierte Daten als CSV exportieren',
11910 clearAllFilters: 'Alle filter reinigen'
11911 },
11912 importer: {
11913 noHeaders: 'Es konnten keine Spaltennamen ermittelt werden. Sind in der Datei Spaltendefinitionen enthalten?',
11914 noObjects: 'Es konnten keine Zeileninformationen gelesen werden, Sind in der Datei außer den Spaltendefinitionen auch Daten enthalten?',
11915 invalidCsv: 'Die Datei konnte nicht eingelesen werden, ist es eine gültige CSV-Datei?',
11916 invalidJson: 'Die Datei konnte nicht eingelesen werden. Enthält sie gültiges JSON?',
11917 jsonNotArray: 'Die importierte JSON-Datei muß ein Array enthalten. Breche Import ab.'
11918 },
11919 pagination: {
11920 sizes: 'Einträge pro Seite',
11921 totalItems: 'Einträge'
11922 },
11923 grouping: {
11924 group: 'Gruppieren',
11925 ungroup: 'Gruppierung aufheben',
11926 aggregate_count: 'Agg: Anzahl',
11927 aggregate_sum: 'Agg: Summe',
11928 aggregate_max: 'Agg: Maximum',
11929 aggregate_min: 'Agg: Minimum',
11930 aggregate_avg: 'Agg: Mittelwert',
11931 aggregate_remove: 'Aggregation entfernen'
11932 }
11933 });
11934 return $delegate;
11935 }]);
11936 }]);
11937 })();
11938
11939 (function () {
11940 angular.module('ui.grid').config(['$provide', function($provide) {
11941 $provide.decorator('i18nService', ['$delegate', function($delegate) {
11942 $delegate.add('en', {
11943 headerCell: {
11944 aria: {
11945 defaultFilterLabel: 'Filter for column',
11946 removeFilter: 'Remove Filter',
11947 columnMenuButtonLabel: 'Column Menu'
11948 },
11949 priority: 'Priority:',
11950 filterLabel: "Filter for column: "
11951 },
11952 aggregate: {
11953 label: 'items'
11954 },
11955 groupPanel: {
11956 description: 'Drag a column header here and drop it to group by that column.'
11957 },
11958 search: {
11959 placeholder: 'Search...',
11960 showingItems: 'Showing Items:',
11961 selectedItems: 'Selected Items:',
11962 totalItems: 'Total Items:',
11963 size: 'Page Size:',
11964 first: 'First Page',
11965 next: 'Next Page',
11966 previous: 'Previous Page',
11967 last: 'Last Page'
11968 },
11969 menu: {
11970 text: 'Choose Columns:'
11971 },
11972 sort: {
11973 ascending: 'Sort Ascending',
11974 descending: 'Sort Descending',
11975 none: 'Sort None',
11976 remove: 'Remove Sort'
11977 },
11978 column: {
11979 hide: 'Hide Column'
11980 },
11981 aggregation: {
11982 count: 'total rows: ',
11983 sum: 'total: ',
11984 avg: 'avg: ',
11985 min: 'min: ',
11986 max: 'max: '
11987 },
11988 pinning: {
11989 pinLeft: 'Pin Left',
11990 pinRight: 'Pin Right',
11991 unpin: 'Unpin'
11992 },
11993 columnMenu: {
11994 close: 'Close'
11995 },
11996 gridMenu: {
11997 aria: {
11998 buttonLabel: 'Grid Menu'
11999 },
12000 columns: 'Columns:',
12001 importerTitle: 'Import file',
12002 exporterAllAsCsv: 'Export all data as csv',
12003 exporterVisibleAsCsv: 'Export visible data as csv',
12004 exporterSelectedAsCsv: 'Export selected data as csv',
12005 exporterAllAsPdf: 'Export all data as pdf',
12006 exporterVisibleAsPdf: 'Export visible data as pdf',
12007 exporterSelectedAsPdf: 'Export selected data as pdf',
12008 clearAllFilters: 'Clear all filters'
12009 },
12010 importer: {
12011 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12012 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12013 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12014 invalidJson: 'File was unable to be processed, is it valid Json?',
12015 jsonNotArray: 'Imported json file must contain an array, aborting.'
12016 },
12017 pagination: {
12018 aria: {
12019 pageToFirst: 'Page to first',
12020 pageBack: 'Page back',
12021 pageSelected: 'Selected page',
12022 pageForward: 'Page forward',
12023 pageToLast: 'Page to last'
12024 },
12025 sizes: 'items per page',
12026 totalItems: 'items',
12027 through: 'through',
12028 of: 'of'
12029 },
12030 grouping: {
12031 group: 'Group',
12032 ungroup: 'Ungroup',
12033 aggregate_count: 'Agg: Count',
12034 aggregate_sum: 'Agg: Sum',
12035 aggregate_max: 'Agg: Max',
12036 aggregate_min: 'Agg: Min',
12037 aggregate_avg: 'Agg: Avg',
12038 aggregate_remove: 'Agg: Remove'
12039 }
12040 });
12041 return $delegate;
12042 }]);
12043 }]);
12044 })();
12045
12046 (function () {
12047 angular.module('ui.grid').config(['$provide', function($provide) {
12048 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12049 $delegate.add('es', {
12050 aggregate: {
12051 label: 'Artículos'
12052 },
12053 groupPanel: {
12054 description: 'Arrastre un encabezado de columna aquí y suéltelo para agrupar por esa columna.'
12055 },
12056 search: {
12057 placeholder: 'Buscar...',
12058 showingItems: 'Artículos Mostrados:',
12059 selectedItems: 'Artículos Seleccionados:',
12060 totalItems: 'Artículos Totales:',
12061 size: 'Tamaño de Página:',
12062 first: 'Primera Página',
12063 next: 'Página Siguiente',
12064 previous: 'Página Anterior',
12065 last: 'Última Página'
12066 },
12067 menu: {
12068 text: 'Elegir columnas:'
12069 },
12070 sort: {
12071 ascending: 'Orden Ascendente',
12072 descending: 'Orden Descendente',
12073 remove: 'Sin Ordenar'
12074 },
12075 column: {
12076 hide: 'Ocultar la columna'
12077 },
12078 aggregation: {
12079 count: 'filas totales: ',
12080 sum: 'total: ',
12081 avg: 'media: ',
12082 min: 'min: ',
12083 max: 'max: '
12084 },
12085 pinning: {
12086 pinLeft: 'Fijar a la Izquierda',
12087 pinRight: 'Fijar a la Derecha',
12088 unpin: 'Quitar Fijación'
12089 },
12090 gridMenu: {
12091 columns: 'Columnas:',
12092 importerTitle: 'Importar archivo',
12093 exporterAllAsCsv: 'Exportar todo como csv',
12094 exporterVisibleAsCsv: 'Exportar vista como csv',
12095 exporterSelectedAsCsv: 'Exportar selección como csv',
12096 exporterAllAsPdf: 'Exportar todo como pdf',
12097 exporterVisibleAsPdf: 'Exportar vista como pdf',
12098 exporterSelectedAsPdf: 'Exportar selección como pdf',
12099 clearAllFilters: 'Limpiar todos los filtros'
12100 },
12101 importer: {
12102 noHeaders: 'No fue posible derivar los nombres de las columnas, ¿tiene encabezados el archivo?',
12103 noObjects: 'No fue posible obtener registros, ¿contiene datos el archivo, aparte de los encabezados?',
12104 invalidCsv: 'No fue posible procesar el archivo, ¿es un CSV válido?',
12105 invalidJson: 'No fue posible procesar el archivo, ¿es un Json válido?',
12106 jsonNotArray: 'El archivo json importado debe contener un array, abortando.'
12107 },
12108 pagination: {
12109 sizes: 'registros por página',
12110 totalItems: 'registros',
12111 of: 'de'
12112 },
12113 grouping: {
12114 group: 'Agrupar',
12115 ungroup: 'Desagrupar',
12116 aggregate_count: 'Agr: Cont',
12117 aggregate_sum: 'Agr: Sum',
12118 aggregate_max: 'Agr: Máx',
12119 aggregate_min: 'Agr: Min',
12120 aggregate_avg: 'Agr: Prom',
12121 aggregate_remove: 'Agr: Quitar'
12122 }
12123 });
12124 return $delegate;
12125 }]);
12126 }]);
12127 })();
12128
12129 /**
12130 * Translated by: R. Salarmehr
12131 * M. Hosseynzade
12132 * Using Vajje.com online dictionary.
12133 */
12134 (function () {
12135 angular.module('ui.grid').config(['$provide', function ($provide) {
12136 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12137 $delegate.add('fa', {
12138 aggregate: {
12139 label: 'قلم'
12140 },
12141 groupPanel: {
12142 description: 'عنوان یک ستون را بگیر و به گروهی از آن ستون رها کن.'
12143 },
12144 search: {
12145 placeholder: 'جستجو...',
12146 showingItems: 'نمایش اقلام:',
12147 selectedItems: 'قلم\u200cهای انتخاب شده:',
12148 totalItems: 'مجموع اقلام:',
12149 size: 'اندازه\u200cی صفحه:',
12150 first: 'اولین صفحه',
12151 next: 'صفحه\u200cی\u200cبعدی',
12152 previous: 'صفحه\u200cی\u200c قبلی',
12153 last: 'آخرین صفحه'
12154 },
12155 menu: {
12156 text: 'ستون\u200cهای انتخابی:'
12157 },
12158 sort: {
12159 ascending: 'ترتیب صعودی',
12160 descending: 'ترتیب نزولی',
12161 remove: 'حذف مرتب کردن'
12162 },
12163 column: {
12164 hide: 'پنهان\u200cکردن ستون'
12165 },
12166 aggregation: {
12167 count: 'تعداد: ',
12168 sum: 'مجموع: ',
12169 avg: 'میانگین: ',
12170 min: 'کمترین: ',
12171 max: 'بیشترین: '
12172 },
12173 pinning: {
12174 pinLeft: 'پین کردن سمت چپ',
12175 pinRight: 'پین کردن سمت راست',
12176 unpin: 'حذف پین'
12177 },
12178 gridMenu: {
12179 columns: 'ستون\u200cها:',
12180 importerTitle: 'وارد کردن فایل',
12181 exporterAllAsCsv: 'خروجی تمام داده\u200cها در فایل csv',
12182 exporterVisibleAsCsv: 'خروجی داده\u200cهای قابل مشاهده در فایل csv',
12183 exporterSelectedAsCsv: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل csv',
12184 exporterAllAsPdf: 'خروجی تمام داده\u200cها در فایل pdf',
12185 exporterVisibleAsPdf: 'خروجی داده\u200cهای قابل مشاهده در فایل pdf',
12186 exporterSelectedAsPdf: 'خروجی داده\u200cهای انتخاب\u200cشده در فایل pdf',
12187 clearAllFilters: 'پاک کردن تمام فیلتر'
12188 },
12189 importer: {
12190 noHeaders: 'نام ستون قابل استخراج نیست. آیا فایل عنوان دارد؟',
12191 noObjects: 'اشیا قابل استخراج نیستند. آیا به جز عنوان\u200cها در فایل داده وجود دارد؟',
12192 invalidCsv: 'فایل قابل پردازش نیست. آیا فرمت csv معتبر است؟',
12193 invalidJson: 'فایل قابل پردازش نیست. آیا فرمت json معتبر است؟',
12194 jsonNotArray: 'فایل json وارد شده باید حاوی آرایه باشد. عملیات ساقط شد.'
12195 },
12196 pagination: {
12197 sizes: 'اقلام در هر صفحه',
12198 totalItems: 'اقلام',
12199 of: 'از'
12200 },
12201 grouping: {
12202 group: 'گروه\u200cبندی',
12203 ungroup: 'حذف گروه\u200cبندی',
12204 aggregate_count: 'Agg: تعداد',
12205 aggregate_sum: 'Agg: جمع',
12206 aggregate_max: 'Agg: بیشینه',
12207 aggregate_min: 'Agg: کمینه',
12208 aggregate_avg: 'Agg: میانگین',
12209 aggregate_remove: 'Agg: حذف'
12210 }
12211 });
12212 return $delegate;
12213 }]);
12214 }]);
12215 })();
12216
12217 (function () {
12218 angular.module('ui.grid').config(['$provide', function($provide) {
12219 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12220 $delegate.add('fi', {
12221 aggregate: {
12222 label: 'rivit'
12223 },
12224 groupPanel: {
12225 description: 'Raahaa ja pudota otsikko tähän ryhmittääksesi sarakkeen mukaan.'
12226 },
12227 search: {
12228 placeholder: 'Hae...',
12229 showingItems: 'Näytetään rivejä:',
12230 selectedItems: 'Valitut rivit:',
12231 totalItems: 'Rivejä yht.:',
12232 size: 'Näytä:',
12233 first: 'Ensimmäinen sivu',
12234 next: 'Seuraava sivu',
12235 previous: 'Edellinen sivu',
12236 last: 'Viimeinen sivu'
12237 },
12238 menu: {
12239 text: 'Valitse sarakkeet:'
12240 },
12241 sort: {
12242 ascending: 'Järjestä nouseva',
12243 descending: 'Järjestä laskeva',
12244 remove: 'Poista järjestys'
12245 },
12246 column: {
12247 hide: 'Piilota sarake'
12248 },
12249 aggregation: {
12250 count: 'Rivejä yht.: ',
12251 sum: 'Summa: ',
12252 avg: 'K.a.: ',
12253 min: 'Min: ',
12254 max: 'Max: '
12255 },
12256 pinning: {
12257 pinLeft: 'Lukitse vasemmalle',
12258 pinRight: 'Lukitse oikealle',
12259 unpin: 'Poista lukitus'
12260 },
12261 gridMenu: {
12262 columns: 'Sarakkeet:',
12263 importerTitle: 'Tuo tiedosto',
12264 exporterAllAsCsv: 'Vie tiedot csv-muodossa',
12265 exporterVisibleAsCsv: 'Vie näkyvä tieto csv-muodossa',
12266 exporterSelectedAsCsv: 'Vie valittu tieto csv-muodossa',
12267 exporterAllAsPdf: 'Vie tiedot pdf-muodossa',
12268 exporterVisibleAsPdf: 'Vie näkyvä tieto pdf-muodossa',
12269 exporterSelectedAsPdf: 'Vie valittu tieto pdf-muodossa',
12270 clearAllFilters: 'Puhdista kaikki suodattimet'
12271 },
12272 importer: {
12273 noHeaders: 'Sarakkeen nimiä ei voitu päätellä, onko tiedostossa otsikkoriviä?',
12274 noObjects: 'Tietoja ei voitu lukea, onko tiedostossa muuta kuin otsikkot?',
12275 invalidCsv: 'Tiedostoa ei voitu käsitellä, oliko se CSV-muodossa?',
12276 invalidJson: 'Tiedostoa ei voitu käsitellä, oliko se JSON-muodossa?',
12277 jsonNotArray: 'Tiedosto ei sisältänyt taulukkoa, lopetetaan.'
12278 }
12279 });
12280 return $delegate;
12281 }]);
12282 }]);
12283 })();
12284
12285 (function () {
12286 angular.module('ui.grid').config(['$provide', function($provide) {
12287 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12288 $delegate.add('fr', {
12289 aggregate: {
12290 label: 'éléments'
12291 },
12292 groupPanel: {
12293 description: 'Faites glisser une en-tête de colonne ici pour créer un groupe de colonnes.'
12294 },
12295 search: {
12296 placeholder: 'Recherche...',
12297 showingItems: 'Affichage des éléments :',
12298 selectedItems: 'Éléments sélectionnés :',
12299 totalItems: 'Nombre total d\'éléments:',
12300 size: 'Taille de page:',
12301 first: 'Première page',
12302 next: 'Page Suivante',
12303 previous: 'Page précédente',
12304 last: 'Dernière page'
12305 },
12306 menu: {
12307 text: 'Choisir des colonnes :'
12308 },
12309 sort: {
12310 ascending: 'Trier par ordre croissant',
12311 descending: 'Trier par ordre décroissant',
12312 remove: 'Enlever le tri'
12313 },
12314 column: {
12315 hide: 'Cacher la colonne'
12316 },
12317 aggregation: {
12318 count: 'lignes totales: ',
12319 sum: 'total: ',
12320 avg: 'moy: ',
12321 min: 'min: ',
12322 max: 'max: '
12323 },
12324 pinning: {
12325 pinLeft: 'Épingler à gauche',
12326 pinRight: 'Épingler à droite',
12327 unpin: 'Détacher'
12328 },
12329 gridMenu: {
12330 columns: 'Colonnes:',
12331 importerTitle: 'Importer un fichier',
12332 exporterAllAsCsv: 'Exporter toutes les données en CSV',
12333 exporterVisibleAsCsv: 'Exporter les données visibles en CSV',
12334 exporterSelectedAsCsv: 'Exporter les données sélectionnées en CSV',
12335 exporterAllAsPdf: 'Exporter toutes les données en PDF',
12336 exporterVisibleAsPdf: 'Exporter les données visibles en PDF',
12337 exporterSelectedAsPdf: 'Exporter les données sélectionnées en PDF',
12338 clearAllFilters: 'Nettoyez tous les filtres'
12339 },
12340 importer: {
12341 noHeaders: 'Impossible de déterminer le nom des colonnes, le fichier possède-t-il une en-tête ?',
12342 noObjects: 'Aucun objet trouvé, le fichier possède-t-il des données autres que l\'en-tête ?',
12343 invalidCsv: 'Le fichier n\'a pas pu être traité, le CSV est-il valide ?',
12344 invalidJson: 'Le fichier n\'a pas pu être traité, le JSON est-il valide ?',
12345 jsonNotArray: 'Le fichier JSON importé doit contenir un tableau, abandon.'
12346 },
12347 pagination: {
12348 sizes: 'éléments par page',
12349 totalItems: 'éléments',
12350 of: 'sur'
12351 },
12352 grouping: {
12353 group: 'Grouper',
12354 ungroup: 'Dégrouper',
12355 aggregate_count: 'Agg: Compte',
12356 aggregate_sum: 'Agg: Somme',
12357 aggregate_max: 'Agg: Max',
12358 aggregate_min: 'Agg: Min',
12359 aggregate_avg: 'Agg: Moy',
12360 aggregate_remove: 'Agg: Retirer'
12361 }
12362 });
12363 return $delegate;
12364 }]);
12365 }]);
12366 })();
12367
12368 (function () {
12369 angular.module('ui.grid').config(['$provide', function ($provide) {
12370 $provide.decorator('i18nService', ['$delegate', function ($delegate) {
12371 $delegate.add('he', {
12372 aggregate: {
12373 label: 'items'
12374 },
12375 groupPanel: {
12376 description: 'גרור עמודה לכאן ושחרר בכדי לקבץ עמודה זו.'
12377 },
12378 search: {
12379 placeholder: 'חפש...',
12380 showingItems: 'מציג:',
12381 selectedItems: 'סה"כ נבחרו:',
12382 totalItems: 'סה"כ רשומות:',
12383 size: 'תוצאות בדף:',
12384 first: 'דף ראשון',
12385 next: 'דף הבא',
12386 previous: 'דף קודם',
12387 last: 'דף אחרון'
12388 },
12389 menu: {
12390 text: 'בחר עמודות:'
12391 },
12392 sort: {
12393 ascending: 'סדר עולה',
12394 descending: 'סדר יורד',
12395 remove: 'בטל'
12396 },
12397 column: {
12398 hide: 'טור הסתר'
12399 },
12400 aggregation: {
12401 count: 'total rows: ',
12402 sum: 'total: ',
12403 avg: 'avg: ',
12404 min: 'min: ',
12405 max: 'max: '
12406 },
12407 gridMenu: {
12408 columns: 'Columns:',
12409 importerTitle: 'Import file',
12410 exporterAllAsCsv: 'Export all data as csv',
12411 exporterVisibleAsCsv: 'Export visible data as csv',
12412 exporterSelectedAsCsv: 'Export selected data as csv',
12413 exporterAllAsPdf: 'Export all data as pdf',
12414 exporterVisibleAsPdf: 'Export visible data as pdf',
12415 exporterSelectedAsPdf: 'Export selected data as pdf',
12416 clearAllFilters: 'Clean all filters'
12417 },
12418 importer: {
12419 noHeaders: 'Column names were unable to be derived, does the file have a header?',
12420 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
12421 invalidCsv: 'File was unable to be processed, is it valid CSV?',
12422 invalidJson: 'File was unable to be processed, is it valid Json?',
12423 jsonNotArray: 'Imported json file must contain an array, aborting.'
12424 }
12425 });
12426 return $delegate;
12427 }]);
12428 }]);
12429 })();
12430
12431 (function () {
12432 angular.module('ui.grid').config(['$provide', function($provide) {
12433 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12434 $delegate.add('hy', {
12435 aggregate: {
12436 label: 'տվյալներ'
12437 },
12438 groupPanel: {
12439 description: 'Ըստ սյան խմբավորելու համար քաշեք և գցեք վերնագիրն այստեղ։'
12440 },
12441 search: {
12442 placeholder: 'Փնտրում...',
12443 showingItems: 'Ցուցադրված տվյալներ՝',
12444 selectedItems: 'Ընտրված:',
12445 totalItems: 'Ընդամենը՝',
12446 size: 'Տողերի քանակը էջում՝',
12447 first: 'Առաջին էջ',
12448 next: 'Հաջորդ էջ',
12449 previous: 'Նախորդ էջ',
12450 last: 'Վերջին էջ'
12451 },
12452 menu: {
12453 text: 'Ընտրել սյուները:'
12454 },
12455 sort: {
12456 ascending: 'Աճման կարգով',
12457 descending: 'Նվազման կարգով',
12458 remove: 'Հանել '
12459 },
12460 column: {
12461 hide: 'Թաքցնել սյունը'
12462 },
12463 aggregation: {
12464 count: 'ընդամենը տող՝ ',
12465 sum: 'ընդամենը՝ ',
12466 avg: 'միջին՝ ',
12467 min: 'մին՝ ',
12468 max: 'մաքս՝ '
12469 },
12470 pinning: {
12471 pinLeft: 'Կպցնել ձախ կողմում',
12472 pinRight: 'Կպցնել աջ կողմում',
12473 unpin: 'Արձակել'
12474 },
12475 gridMenu: {
12476 columns: 'Սյուներ:',
12477 importerTitle: 'Ներմուծել ֆայլ',
12478 exporterAllAsCsv: 'Արտահանել ամբողջը CSV',
12479 exporterVisibleAsCsv: 'Արտահանել երևացող տվյալները CSV',
12480 exporterSelectedAsCsv: 'Արտահանել ընտրված տվյալները CSV',
12481 exporterAllAsPdf: 'Արտահանել PDF',
12482 exporterVisibleAsPdf: 'Արտահանել երևացող տվյալները PDF',
12483 exporterSelectedAsPdf: 'Արտահանել ընտրված տվյալները PDF',
12484 clearAllFilters: 'Մաքրել բոլոր ֆիլտրերը'
12485 },
12486 importer: {
12487 noHeaders: 'Հնարավոր չեղավ որոշել սյան վերնագրերը։ Արդյո՞ք ֆայլը ունի վերնագրեր։',
12488 noObjects: 'Հնարավոր չեղավ կարդալ տվյալները։ Արդյո՞ք ֆայլում կան տվյալներ։',
12489 invalidCsv: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր CSV է։',
12490 invalidJson: 'Հնարավոր չեղավ մշակել ֆայլը։ Արդյո՞ք այն վավեր Json է։',
12491 jsonNotArray: 'Ներմուծված json ֆայլը պետք է պարունակի զանգված, կասեցվում է։'
12492 }
12493 });
12494 return $delegate;
12495 }]);
12496 }]);
12497 })();
12498
12499 (function () {
12500 angular.module('ui.grid').config(['$provide', function($provide) {
12501 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12502 $delegate.add('it', {
12503 aggregate: {
12504 label: 'elementi'
12505 },
12506 groupPanel: {
12507 description: 'Trascina un\'intestazione all\'interno del gruppo della colonna.'
12508 },
12509 search: {
12510 placeholder: 'Ricerca...',
12511 showingItems: 'Mostra:',
12512 selectedItems: 'Selezionati:',
12513 totalItems: 'Totali:',
12514 size: 'Tot Pagine:',
12515 first: 'Prima',
12516 next: 'Prossima',
12517 previous: 'Precedente',
12518 last: 'Ultima'
12519 },
12520 menu: {
12521 text: 'Scegli le colonne:'
12522 },
12523 sort: {
12524 ascending: 'Asc.',
12525 descending: 'Desc.',
12526 remove: 'Annulla ordinamento'
12527 },
12528 column: {
12529 hide: 'Nascondi'
12530 },
12531 aggregation: {
12532 count: 'righe totali: ',
12533 sum: 'tot: ',
12534 avg: 'media: ',
12535 min: 'minimo: ',
12536 max: 'massimo: '
12537 },
12538 pinning: {
12539 pinLeft: 'Blocca a sx',
12540 pinRight: 'Blocca a dx',
12541 unpin: 'Blocca in alto'
12542 },
12543 gridMenu: {
12544 columns: 'Colonne:',
12545 importerTitle: 'Importa',
12546 exporterAllAsCsv: 'Esporta tutti i dati in CSV',
12547 exporterVisibleAsCsv: 'Esporta i dati visibili in CSV',
12548 exporterSelectedAsCsv: 'Esporta i dati selezionati in CSV',
12549 exporterAllAsPdf: 'Esporta tutti i dati in PDF',
12550 exporterVisibleAsPdf: 'Esporta i dati visibili in PDF',
12551 exporterSelectedAsPdf: 'Esporta i dati selezionati in PDF',
12552 clearAllFilters: 'Pulire tutti i filtri'
12553 },
12554 importer: {
12555 noHeaders: 'Impossibile reperire i nomi delle colonne, sicuro che siano indicati all\'interno del file?',
12556 noObjects: 'Impossibile reperire gli oggetti, sicuro che siano indicati all\'interno del file?',
12557 invalidCsv: 'Impossibile elaborare il file, sicuro che sia un CSV?',
12558 invalidJson: 'Impossibile elaborare il file, sicuro che sia un JSON valido?',
12559 jsonNotArray: 'Errore! Il file JSON da importare deve contenere un array.'
12560 },
12561 grouping: {
12562 group: 'Raggruppa',
12563 ungroup: 'Separa',
12564 aggregate_count: 'Agg: N. Elem.',
12565 aggregate_sum: 'Agg: Somma',
12566 aggregate_max: 'Agg: Massimo',
12567 aggregate_min: 'Agg: Minimo',
12568 aggregate_avg: 'Agg: Media',
12569 aggregate_remove: 'Agg: Rimuovi'
12570 }
12571 });
12572 return $delegate;
12573 }]);
12574 }]);
12575 })();
12576
12577 (function() {
12578 angular.module('ui.grid').config(['$provide', function($provide) {
12579 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12580 $delegate.add('ja', {
12581 aggregate: {
12582 label: '項目'
12583 },
12584 groupPanel: {
12585 description: 'ここに列ヘッダをドラッグアンドドロップして、その列でグループ化します。'
12586 },
12587 search: {
12588 placeholder: '検索...',
12589 showingItems: '表示中の項目:',
12590 selectedItems: '選択した項目:',
12591 totalItems: '項目の総数:',
12592 size: 'ページサイズ:',
12593 first: '最初のページ',
12594 next: '次のページ',
12595 previous: '前のページ',
12596 last: '前のページ'
12597 },
12598 menu: {
12599 text: '列の選択:'
12600 },
12601 sort: {
12602 ascending: '昇順に並べ替え',
12603 descending: '降順に並べ替え',
12604 remove: '並べ替えの解除'
12605 },
12606 column: {
12607 hide: '列の非表示'
12608 },
12609 aggregation: {
12610 count: '合計行数: ',
12611 sum: '合計: ',
12612 avg: '平均: ',
12613 min: '最小: ',
12614 max: '最大: '
12615 },
12616 pinning: {
12617 pinLeft: '左に固定',
12618 pinRight: '右に固定',
12619 unpin: '固定解除'
12620 },
12621 gridMenu: {
12622 columns: '列:',
12623 importerTitle: 'ファイルのインポート',
12624 exporterAllAsCsv: 'すべてのデータをCSV形式でエクスポート',
12625 exporterVisibleAsCsv: '表示中のデータをCSV形式でエクスポート',
12626 exporterSelectedAsCsv: '選択したデータをCSV形式でエクスポート',
12627 exporterAllAsPdf: 'すべてのデータをPDF形式でエクスポート',
12628 exporterVisibleAsPdf: '表示中のデータをPDF形式でエクスポート',
12629 exporterSelectedAsPdf: '選択したデータをPDF形式でエクスポート',
12630 clearAllFilters: 'すべてのフィルタを清掃してください'
12631 },
12632 importer: {
12633 noHeaders: '列名を取得できません。ファイルにヘッダが含まれていることを確認してください。',
12634 noObjects: 'オブジェクトを取得できません。ファイルにヘッダ以外のデータが含まれていることを確認してください。',
12635 invalidCsv: 'ファイルを処理できません。ファイルが有効なCSV形式であることを確認してください。',
12636 invalidJson: 'ファイルを処理できません。ファイルが有効なJSON形式であることを確認してください。',
12637 jsonNotArray: 'インポートしたJSONファイルには配列が含まれている必要があります。処理を中止します。'
12638 },
12639 pagination: {
12640 sizes: '項目/ページ',
12641 totalItems: '項目'
12642 }
12643 });
12644 return $delegate;
12645 }]);
12646 }]);
12647 })();
12648
12649 (function () {
12650 angular.module('ui.grid').config(['$provide', function($provide) {
12651 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12652 $delegate.add('ko', {
12653 aggregate: {
12654 label: '아이템'
12655 },
12656 groupPanel: {
12657 description: '컬럼으로 그룹핑하기 위해서는 컬럼 헤더를 끌어 떨어뜨려 주세요.'
12658 },
12659 search: {
12660 placeholder: '검색...',
12661 showingItems: '항목 보여주기:',
12662 selectedItems: '선택 항목:',
12663 totalItems: '전체 항목:',
12664 size: '페이지 크기:',
12665 first: '첫번째 페이지',
12666 next: '다음 페이지',
12667 previous: '이전 페이지',
12668 last: '마지막 페이지'
12669 },
12670 menu: {
12671 text: '컬럼을 선택하세요:'
12672 },
12673 sort: {
12674 ascending: '오름차순 정렬',
12675 descending: '내림차순 정렬',
12676 remove: '소팅 제거'
12677 },
12678 column: {
12679 hide: '컬럼 제거'
12680 },
12681 aggregation: {
12682 count: '전체 갯수: ',
12683 sum: '전체: ',
12684 avg: '평균: ',
12685 min: '최소: ',
12686 max: '최대: '
12687 },
12688 pinning: {
12689 pinLeft: '왼쪽 핀',
12690 pinRight: '오른쪽 핀',
12691 unpin: '핀 제거'
12692 },
12693 gridMenu: {
12694 columns: '컬럼:',
12695 importerTitle: '파일 가져오기',
12696 exporterAllAsCsv: 'csv로 모든 데이터 내보내기',
12697 exporterVisibleAsCsv: 'csv로 보이는 데이터 내보내기',
12698 exporterSelectedAsCsv: 'csv로 선택된 데이터 내보내기',
12699 exporterAllAsPdf: 'pdf로 모든 데이터 내보내기',
12700 exporterVisibleAsPdf: 'pdf로 보이는 데이터 내보내기',
12701 exporterSelectedAsPdf: 'pdf로 선택 데이터 내보내기',
12702 clearAllFilters: '모든 필터를 청소'
12703 },
12704 importer: {
12705 noHeaders: '컬럼명이 지정되어 있지 않습니다. 파일에 헤더가 명시되어 있는지 확인해 주세요.',
12706 noObjects: '데이터가 지정되어 있지 않습니다. 데이터가 파일에 있는지 확인해 주세요.',
12707 invalidCsv: '파일을 처리할 수 없습니다. 올바른 csv인지 확인해 주세요.',
12708 invalidJson: '파일을 처리할 수 없습니다. 올바른 json인지 확인해 주세요.',
12709 jsonNotArray: 'json 파일은 배열을 포함해야 합니다.'
12710 },
12711 pagination: {
12712 sizes: '페이지당 항목',
12713 totalItems: '전체 항목'
12714 }
12715 });
12716 return $delegate;
12717 }]);
12718 }]);
12719 })();
12720
12721 (function () {
12722 angular.module('ui.grid').config(['$provide', function($provide) {
12723 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12724 $delegate.add('nl', {
12725 aggregate: {
12726 label: 'items'
12727 },
12728 groupPanel: {
12729 description: 'Sleep hier een kolomnaam heen om op te groeperen.'
12730 },
12731 search: {
12732 placeholder: 'Zoeken...',
12733 showingItems: 'Getoonde items:',
12734 selectedItems: 'Geselecteerde items:',
12735 totalItems: 'Totaal aantal items:',
12736 size: 'Items per pagina:',
12737 first: 'Eerste pagina',
12738 next: 'Volgende pagina',
12739 previous: 'Vorige pagina',
12740 last: 'Laatste pagina'
12741 },
12742 menu: {
12743 text: 'Kies kolommen:'
12744 },
12745 sort: {
12746 ascending: 'Sorteer oplopend',
12747 descending: 'Sorteer aflopend',
12748 remove: 'Verwijder sortering'
12749 },
12750 column: {
12751 hide: 'Verberg kolom'
12752 },
12753 aggregation: {
12754 count: 'Aantal rijen: ',
12755 sum: 'Som: ',
12756 avg: 'Gemiddelde: ',
12757 min: 'Min: ',
12758 max: 'Max: '
12759 },
12760 pinning: {
12761 pinLeft: 'Zet links vast',
12762 pinRight: 'Zet rechts vast',
12763 unpin: 'Maak los'
12764 },
12765 gridMenu: {
12766 columns: 'Kolommen:',
12767 importerTitle: 'Importeer bestand',
12768 exporterAllAsCsv: 'Exporteer alle data als csv',
12769 exporterVisibleAsCsv: 'Exporteer zichtbare data als csv',
12770 exporterSelectedAsCsv: 'Exporteer geselecteerde data als csv',
12771 exporterAllAsPdf: 'Exporteer alle data als pdf',
12772 exporterVisibleAsPdf: 'Exporteer zichtbare data als pdf',
12773 exporterSelectedAsPdf: 'Exporteer geselecteerde data als pdf',
12774 clearAllFilters: 'Reinig alle filters'
12775 },
12776 importer: {
12777 noHeaders: 'Kolomnamen kunnen niet worden afgeleid. Heeft het bestand een header?',
12778 noObjects: 'Objecten kunnen niet worden afgeleid. Bevat het bestand data naast de headers?',
12779 invalidCsv: 'Het bestand kan niet verwerkt worden. Is het een valide csv bestand?',
12780 invalidJson: 'Het bestand kan niet verwerkt worden. Is het valide json?',
12781 jsonNotArray: 'Het json bestand moet een array bevatten. De actie wordt geannuleerd.'
12782 },
12783 pagination: {
12784 sizes: 'items per pagina',
12785 totalItems: 'items',
12786 of: 'van de'
12787 },
12788 grouping: {
12789 group: 'Groepeer',
12790 ungroup: 'Groepering opheffen',
12791 aggregate_count: 'Agg: Aantal',
12792 aggregate_sum: 'Agg: Som',
12793 aggregate_max: 'Agg: Max',
12794 aggregate_min: 'Agg: Min',
12795 aggregate_avg: 'Agg: Gem',
12796 aggregate_remove: 'Agg: Verwijder'
12797 }
12798 });
12799 return $delegate;
12800 }]);
12801 }]);
12802 })();
12803
12804 (function () {
12805 angular.module('ui.grid').config(['$provide', function($provide) {
12806 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12807 $delegate.add('pt-br', {
12808 aggregate: {
12809 label: 'itens'
12810 },
12811 groupPanel: {
12812 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
12813 },
12814 search: {
12815 placeholder: 'Procurar...',
12816 showingItems: 'Mostrando os Itens:',
12817 selectedItems: 'Items Selecionados:',
12818 totalItems: 'Total de Itens:',
12819 size: 'Tamanho da Página:',
12820 first: 'Primeira Página',
12821 next: 'Próxima Página',
12822 previous: 'Página Anterior',
12823 last: 'Última Página'
12824 },
12825 menu: {
12826 text: 'Selecione as colunas:'
12827 },
12828 sort: {
12829 ascending: 'Ordenar Ascendente',
12830 descending: 'Ordenar Descendente',
12831 remove: 'Remover Ordenação'
12832 },
12833 column: {
12834 hide: 'Esconder coluna'
12835 },
12836 aggregation: {
12837 count: 'total de linhas: ',
12838 sum: 'total: ',
12839 avg: 'med: ',
12840 min: 'min: ',
12841 max: 'max: '
12842 },
12843 pinning: {
12844 pinLeft: 'Fixar Esquerda',
12845 pinRight: 'Fixar Direita',
12846 unpin: 'Desprender'
12847 },
12848 gridMenu: {
12849 columns: 'Colunas:',
12850 importerTitle: 'Importar arquivo',
12851 exporterAllAsCsv: 'Exportar todos os dados como csv',
12852 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
12853 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
12854 exporterAllAsPdf: 'Exportar todos os dados como pdf',
12855 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
12856 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
12857 clearAllFilters: 'Limpar todos os filtros'
12858 },
12859 importer: {
12860 noHeaders: 'Nomes de colunas não puderam ser derivados. O arquivo tem um cabeçalho?',
12861 noObjects: 'Objetos não puderam ser derivados. Havia dados no arquivo, além dos cabeçalhos?',
12862 invalidCsv: 'Arquivo não pode ser processado. É um CSV válido?',
12863 invalidJson: 'Arquivo não pode ser processado. É um Json válido?',
12864 jsonNotArray: 'Arquivo json importado tem que conter um array. Abortando.'
12865 },
12866 pagination: {
12867 sizes: 'itens por página',
12868 totalItems: 'itens'
12869 },
12870 grouping: {
12871 group: 'Agrupar',
12872 ungroup: 'Desagrupar',
12873 aggregate_count: 'Agr: Contar',
12874 aggregate_sum: 'Agr: Soma',
12875 aggregate_max: 'Agr: Max',
12876 aggregate_min: 'Agr: Min',
12877 aggregate_avg: 'Agr: Med',
12878 aggregate_remove: 'Agr: Remover'
12879 }
12880 });
12881 return $delegate;
12882 }]);
12883 }]);
12884 })();
12885
12886 (function () {
12887 angular.module('ui.grid').config(['$provide', function($provide) {
12888 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12889 $delegate.add('pt', {
12890 aggregate: {
12891 label: 'itens'
12892 },
12893 groupPanel: {
12894 description: 'Arraste e solte uma coluna aqui para agrupar por essa coluna'
12895 },
12896 search: {
12897 placeholder: 'Procurar...',
12898 showingItems: 'Mostrando os Itens:',
12899 selectedItems: 'Itens Selecionados:',
12900 totalItems: 'Total de Itens:',
12901 size: 'Tamanho da Página:',
12902 first: 'Primeira Página',
12903 next: 'Próxima Página',
12904 previous: 'Página Anterior',
12905 last: 'Última Página'
12906 },
12907 menu: {
12908 text: 'Selecione as colunas:'
12909 },
12910 sort: {
12911 ascending: 'Ordenar Ascendente',
12912 descending: 'Ordenar Descendente',
12913 remove: 'Remover Ordenação'
12914 },
12915 column: {
12916 hide: 'Esconder coluna'
12917 },
12918 aggregation: {
12919 count: 'total de linhas: ',
12920 sum: 'total: ',
12921 avg: 'med: ',
12922 min: 'min: ',
12923 max: 'max: '
12924 },
12925 pinning: {
12926 pinLeft: 'Fixar Esquerda',
12927 pinRight: 'Fixar Direita',
12928 unpin: 'Desprender'
12929 },
12930 gridMenu: {
12931 columns: 'Colunas:',
12932 importerTitle: 'Importar ficheiro',
12933 exporterAllAsCsv: 'Exportar todos os dados como csv',
12934 exporterVisibleAsCsv: 'Exportar dados visíveis como csv',
12935 exporterSelectedAsCsv: 'Exportar dados selecionados como csv',
12936 exporterAllAsPdf: 'Exportar todos os dados como pdf',
12937 exporterVisibleAsPdf: 'Exportar dados visíveis como pdf',
12938 exporterSelectedAsPdf: 'Exportar dados selecionados como pdf',
12939 clearAllFilters: 'Limpar todos os filtros'
12940 },
12941 importer: {
12942 noHeaders: 'Nomes de colunas não puderam ser derivados. O ficheiro tem um cabeçalho?',
12943 noObjects: 'Objetos não puderam ser derivados. Havia dados no ficheiro, além dos cabeçalhos?',
12944 invalidCsv: 'Ficheiro não pode ser processado. É um CSV válido?',
12945 invalidJson: 'Ficheiro não pode ser processado. É um Json válido?',
12946 jsonNotArray: 'Ficheiro json importado tem que conter um array. Interrompendo.'
12947 },
12948 pagination: {
12949 sizes: 'itens por página',
12950 totalItems: 'itens',
12951 of: 'de'
12952 },
12953 grouping: {
12954 group: 'Agrupar',
12955 ungroup: 'Desagrupar',
12956 aggregate_count: 'Agr: Contar',
12957 aggregate_sum: 'Agr: Soma',
12958 aggregate_max: 'Agr: Max',
12959 aggregate_min: 'Agr: Min',
12960 aggregate_avg: 'Agr: Med',
12961 aggregate_remove: 'Agr: Remover'
12962 }
12963 });
12964 return $delegate;
12965 }]);
12966 }]);
12967 })();
12968
12969 (function () {
12970 angular.module('ui.grid').config(['$provide', function($provide) {
12971 $provide.decorator('i18nService', ['$delegate', function($delegate) {
12972 $delegate.add('ru', {
12973 aggregate: {
12974 label: 'элементы'
12975 },
12976 groupPanel: {
12977 description: 'Для группировки по столбцу перетащите сюда его название.'
12978 },
12979 search: {
12980 placeholder: 'Поиск...',
12981 showingItems: 'Показать элементы:',
12982 selectedItems: 'Выбранные элементы:',
12983 totalItems: 'Всего элементов:',
12984 size: 'Размер страницы:',
12985 first: 'Первая страница',
12986 next: 'Следующая страница',
12987 previous: 'Предыдущая страница',
12988 last: 'Последняя страница'
12989 },
12990 menu: {
12991 text: 'Выбрать столбцы:'
12992 },
12993 sort: {
12994 ascending: 'По возрастанию',
12995 descending: 'По убыванию',
12996 remove: 'Убрать сортировку'
12997 },
12998 column: {
12999 hide: 'Спрятать столбец'
13000 },
13001 aggregation: {
13002 count: 'всего строк: ',
13003 sum: 'итого: ',
13004 avg: 'среднее: ',
13005 min: 'мин: ',
13006 max: 'макс: '
13007 },
13008 pinning: {
13009 pinLeft: 'Закрепить слева',
13010 pinRight: 'Закрепить справа',
13011 unpin: 'Открепить'
13012 },
13013 gridMenu: {
13014 columns: 'Столбцы:',
13015 importerTitle: 'Import file',
13016 exporterAllAsCsv: 'Экспортировать всё в CSV',
13017 exporterVisibleAsCsv: 'Экспортировать видимые данные в CSV',
13018 exporterSelectedAsCsv: 'Экспортировать выбранные данные в CSV',
13019 exporterAllAsPdf: 'Экспортировать всё в PDF',
13020 exporterVisibleAsPdf: 'Экспортировать видимые данные в PDF',
13021 exporterSelectedAsPdf: 'Экспортировать выбранные данные в PDF',
13022 clearAllFilters: 'Очистите все фильтры'
13023 },
13024 importer: {
13025 noHeaders: 'Column names were unable to be derived, does the file have a header?',
13026 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
13027 invalidCsv: 'File was unable to be processed, is it valid CSV?',
13028 invalidJson: 'File was unable to be processed, is it valid Json?',
13029 jsonNotArray: 'Imported json file must contain an array, aborting.'
13030 }
13031 });
13032 return $delegate;
13033 }]);
13034 }]);
13035 })();
13036
13037 (function () {
13038 angular.module('ui.grid').config(['$provide', function($provide) {
13039 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13040 $delegate.add('sk', {
13041 aggregate: {
13042 label: 'items'
13043 },
13044 groupPanel: {
13045 description: 'Pretiahni sem názov stĺpca pre zoskupenie podľa toho stĺpca.'
13046 },
13047 search: {
13048 placeholder: 'Hľadaj...',
13049 showingItems: 'Zobrazujem položky:',
13050 selectedItems: 'Vybraté položky:',
13051 totalItems: 'Počet položiek:',
13052 size: 'Počet:',
13053 first: 'Prvá strana',
13054 next: 'Ďalšia strana',
13055 previous: 'Predchádzajúca strana',
13056 last: 'Posledná strana'
13057 },
13058 menu: {
13059 text: 'Vyberte stĺpce:'
13060 },
13061 sort: {
13062 ascending: 'Zotriediť vzostupne',
13063 descending: 'Zotriediť zostupne',
13064 remove: 'Vymazať triedenie'
13065 },
13066 aggregation: {
13067 count: 'total rows: ',
13068 sum: 'total: ',
13069 avg: 'avg: ',
13070 min: 'min: ',
13071 max: 'max: '
13072 },
13073 gridMenu: {
13074 columns: 'Columns:',
13075 importerTitle: 'Import file',
13076 exporterAllAsCsv: 'Export all data as csv',
13077 exporterVisibleAsCsv: 'Export visible data as csv',
13078 exporterSelectedAsCsv: 'Export selected data as csv',
13079 exporterAllAsPdf: 'Export all data as pdf',
13080 exporterVisibleAsPdf: 'Export visible data as pdf',
13081 exporterSelectedAsPdf: 'Export selected data as pdf',
13082 clearAllFilters: 'Clear all filters'
13083 },
13084 importer: {
13085 noHeaders: 'Column names were unable to be derived, does the file have a header?',
13086 noObjects: 'Objects were not able to be derived, was there data in the file other than headers?',
13087 invalidCsv: 'File was unable to be processed, is it valid CSV?',
13088 invalidJson: 'File was unable to be processed, is it valid Json?',
13089 jsonNotArray: 'Imported json file must contain an array, aborting.'
13090 }
13091 });
13092 return $delegate;
13093 }]);
13094 }]);
13095 })();
13096
13097 (function () {
13098 angular.module('ui.grid').config(['$provide', function($provide) {
13099 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13100 $delegate.add('sv', {
13101 aggregate: {
13102 label: 'Artiklar'
13103 },
13104 groupPanel: {
13105 description: 'Dra en kolumnrubrik hit och släpp den för att gruppera efter den kolumnen.'
13106 },
13107 search: {
13108 placeholder: 'Sök...',
13109 showingItems: 'Visar artiklar:',
13110 selectedItems: 'Valda artiklar:',
13111 totalItems: 'Antal artiklar:',
13112 size: 'Sidstorlek:',
13113 first: 'Första sidan',
13114 next: 'Nästa sida',
13115 previous: 'Föregående sida',
13116 last: 'Sista sidan'
13117 },
13118 menu: {
13119 text: 'Välj kolumner:'
13120 },
13121 sort: {
13122 ascending: 'Sortera stigande',
13123 descending: 'Sortera fallande',
13124 remove: 'Inaktivera sortering'
13125 },
13126 column: {
13127 hide: 'Göm kolumn'
13128 },
13129 aggregation: {
13130 count: 'Antal rader: ',
13131 sum: 'Summa: ',
13132 avg: 'Genomsnitt: ',
13133 min: 'Min: ',
13134 max: 'Max: '
13135 },
13136 pinning: {
13137 pinLeft: 'Fäst vänster',
13138 pinRight: 'Fäst höger',
13139 unpin: 'Lösgör'
13140 },
13141 gridMenu: {
13142 columns: 'Kolumner:',
13143 importerTitle: 'Importera fil',
13144 exporterAllAsCsv: 'Exportera all data som CSV',
13145 exporterVisibleAsCsv: 'Exportera synlig data som CSV',
13146 exporterSelectedAsCsv: 'Exportera markerad data som CSV',
13147 exporterAllAsPdf: 'Exportera all data som PDF',
13148 exporterVisibleAsPdf: 'Exportera synlig data som PDF',
13149 exporterSelectedAsPdf: 'Exportera markerad data som PDF',
13150 clearAllFilters: 'Rengör alla filter'
13151 },
13152 importer: {
13153 noHeaders: 'Kolumnnamn kunde inte härledas. Har filen ett sidhuvud?',
13154 noObjects: 'Objekt kunde inte härledas. Har filen data undantaget sidhuvud?',
13155 invalidCsv: 'Filen kunde inte behandlas, är den en giltig CSV?',
13156 invalidJson: 'Filen kunde inte behandlas, är den en giltig JSON?',
13157 jsonNotArray: 'Importerad JSON-fil måste innehålla ett fält. Import avbruten.'
13158 },
13159 pagination: {
13160 sizes: 'Artiklar per sida',
13161 totalItems: 'Artiklar'
13162 }
13163 });
13164 return $delegate;
13165 }]);
13166 }]);
13167 })();
13168
13169 (function () {
13170 angular.module('ui.grid').config(['$provide', function($provide) {
13171 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13172 $delegate.add('ta', {
13173 aggregate: {
13174 label: 'உருப்படிகள்'
13175 },
13176 groupPanel: {
13177 description: 'ஒரு பத்தியை குழுவாக அமைக்க அப்பத்தியின் தலைப்பை இங்கே இழுத்து வரவும் '
13178 },
13179 search: {
13180 placeholder: 'தேடல் ...',
13181 showingItems: 'உருப்படிகளை காண்பித்தல்:',
13182 selectedItems: 'தேர்ந்தெடுக்கப்பட்ட உருப்படிகள்:',
13183 totalItems: 'மொத்த உருப்படிகள்:',
13184 size: 'பக்க அளவு: ',
13185 first: 'முதல் பக்கம்',
13186 next: 'அடுத்த பக்கம்',
13187 previous: 'முந்தைய பக்கம் ',
13188 last: 'இறுதி பக்கம்'
13189 },
13190 menu: {
13191 text: 'பத்திகளை தேர்ந்தெடு:'
13192 },
13193 sort: {
13194 ascending: 'மேலிருந்து கீழாக',
13195 descending: 'கீழிருந்து மேலாக',
13196 remove: 'வரிசையை நீக்கு'
13197 },
13198 column: {
13199 hide: 'பத்தியை மறைத்து வை '
13200 },
13201 aggregation: {
13202 count: 'மொத்த வரிகள்:',
13203 sum: 'மொத்தம்: ',
13204 avg: 'சராசரி: ',
13205 min: 'குறைந்தபட்ச: ',
13206 max: 'அதிகபட்ச: '
13207 },
13208 pinning: {
13209 pinLeft: 'இடதுபுறமாக தைக்க ',
13210 pinRight: 'வலதுபுறமாக தைக்க',
13211 unpin: 'பிரி'
13212 },
13213 gridMenu: {
13214 columns: 'பத்திகள்:',
13215 importerTitle: 'கோப்பு : படித்தல்',
13216 exporterAllAsCsv: 'எல்லா தரவுகளையும் கோப்பாக்கு: csv',
13217 exporterVisibleAsCsv: 'இருக்கும் தரவுகளை கோப்பாக்கு: csv',
13218 exporterSelectedAsCsv: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: csv',
13219 exporterAllAsPdf: 'எல்லா தரவுகளையும் கோப்பாக்கு: pdf',
13220 exporterVisibleAsPdf: 'இருக்கும் தரவுகளை கோப்பாக்கு: pdf',
13221 exporterSelectedAsPdf: 'தேர்ந்தெடுத்த தரவுகளை கோப்பாக்கு: pdf',
13222 clearAllFilters: 'Clear all filters'
13223 },
13224 importer: {
13225 noHeaders: 'பத்தியின் தலைப்புகளை பெற இயலவில்லை, கோப்பிற்கு தலைப்பு உள்ளதா?',
13226 noObjects: 'இலக்குகளை உருவாக்க முடியவில்லை, கோப்பில் தலைப்புகளை தவிர தரவு ஏதேனும் உள்ளதா? ',
13227 invalidCsv: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - csv',
13228 invalidJson: 'சரிவர நடைமுறை படுத்த இயலவில்லை, கோப்பு சரிதானா? - json',
13229 jsonNotArray: 'படித்த கோப்பில் வரிசைகள் உள்ளது, நடைமுறை ரத்து செய் : json'
13230 },
13231 pagination: {
13232 sizes : 'உருப்படிகள் / பக்கம்',
13233 totalItems : 'உருப்படிகள் '
13234 },
13235 grouping: {
13236 group : 'குழு',
13237 ungroup : 'பிரி',
13238 aggregate_count : 'மதிப்பீட்டு : எண்ணு',
13239 aggregate_sum : 'மதிப்பீட்டு : கூட்டல்',
13240 aggregate_max : 'மதிப்பீட்டு : அதிகபட்சம்',
13241 aggregate_min : 'மதிப்பீட்டு : குறைந்தபட்சம்',
13242 aggregate_avg : 'மதிப்பீட்டு : சராசரி',
13243 aggregate_remove : 'மதிப்பீட்டு : நீக்கு'
13244 }
13245 });
13246 return $delegate;
13247 }]);
13248 }]);
13249 })();
13250
13251 /**
13252 * @ngdoc overview
13253 * @name ui.grid.i18n
13254 * @description
13255 *
13256 * # ui.grid.i18n
13257 * This module provides i18n functions to ui.grid and any application that wants to use it
13258
13259 *
13260 * <div doc-module-components="ui.grid.i18n"></div>
13261 */
13262
13263 (function () {
13264 var DIRECTIVE_ALIASES = ['uiT', 'uiTranslate'];
13265 var FILTER_ALIASES = ['t', 'uiTranslate'];
13266
13267 var module = angular.module('ui.grid.i18n');
13268
13269
13270 /**
13271 * @ngdoc object
13272 * @name ui.grid.i18n.constant:i18nConstants
13273 *
13274 * @description constants available in i18n module
13275 */
13276 module.constant('i18nConstants', {
13277 MISSING: '[MISSING]',
13278 UPDATE_EVENT: '$uiI18n',
13279
13280 LOCALE_DIRECTIVE_ALIAS: 'uiI18n',
13281 // default to english
13282 DEFAULT_LANG: 'en'
13283 });
13284
13285 // module.config(['$provide', function($provide) {
13286 // $provide.decorator('i18nService', ['$delegate', function($delegate) {}])}]);
13287
13288 /**
13289 * @ngdoc service
13290 * @name ui.grid.i18n.service:i18nService
13291 *
13292 * @description Services for i18n
13293 */
13294 module.service('i18nService', ['$log', 'i18nConstants', '$rootScope',
13295 function ($log, i18nConstants, $rootScope) {
13296
13297 var langCache = {
13298 _langs: {},
13299 current: null,
13300 get: function (lang) {
13301 return this._langs[lang.toLowerCase()];
13302 },
13303 add: function (lang, strings) {
13304 var lower = lang.toLowerCase();
13305 if (!this._langs[lower]) {
13306 this._langs[lower] = {};
13307 }
13308 angular.extend(this._langs[lower], strings);
13309 },
13310 getAllLangs: function () {
13311 var langs = [];
13312 if (!this._langs) {
13313 return langs;
13314 }
13315
13316 for (var key in this._langs) {
13317 langs.push(key);
13318 }
13319
13320 return langs;
13321 },
13322 setCurrent: function (lang) {
13323 this.current = lang.toLowerCase();
13324 },
13325 getCurrentLang: function () {
13326 return this.current;
13327 }
13328 };
13329
13330 var service = {
13331
13332 /**
13333 * @ngdoc service
13334 * @name add
13335 * @methodOf ui.grid.i18n.service:i18nService
13336 * @description Adds the languages and strings to the cache. Decorate this service to
13337 * add more translation strings
13338 * @param {string} lang language to add
13339 * @param {object} stringMaps of strings to add grouped by property names
13340 * @example
13341 * <pre>
13342 * i18nService.add('en', {
13343 * aggregate: {
13344 * label1: 'items',
13345 * label2: 'some more items'
13346 * }
13347 * },
13348 * groupPanel: {
13349 * description: 'Drag a column header here and drop it to group by that column.'
13350 * }
13351 * }
13352 * </pre>
13353 */
13354 add: function (langs, stringMaps) {
13355 if (typeof(langs) === 'object') {
13356 angular.forEach(langs, function (lang) {
13357 if (lang) {
13358 langCache.add(lang, stringMaps);
13359 }
13360 });
13361 } else {
13362 langCache.add(langs, stringMaps);
13363 }
13364 },
13365
13366 /**
13367 * @ngdoc service
13368 * @name getAllLangs
13369 * @methodOf ui.grid.i18n.service:i18nService
13370 * @description return all currently loaded languages
13371 * @returns {array} string
13372 */
13373 getAllLangs: function () {
13374 return langCache.getAllLangs();
13375 },
13376
13377 /**
13378 * @ngdoc service
13379 * @name get
13380 * @methodOf ui.grid.i18n.service:i18nService
13381 * @description return all currently loaded languages
13382 * @param {string} lang to return. If not specified, returns current language
13383 * @returns {object} the translation string maps for the language
13384 */
13385 get: function (lang) {
13386 var language = lang ? lang : service.getCurrentLang();
13387 return langCache.get(language);
13388 },
13389
13390 /**
13391 * @ngdoc service
13392 * @name getSafeText
13393 * @methodOf ui.grid.i18n.service:i18nService
13394 * @description returns the text specified in the path or a Missing text if text is not found
13395 * @param {string} path property path to use for retrieving text from string map
13396 * @param {string} lang to return. If not specified, returns current language
13397 * @returns {object} the translation for the path
13398 * @example
13399 * <pre>
13400 * i18nService.getSafeText('sort.ascending')
13401 * </pre>
13402 */
13403 getSafeText: function (path, lang) {
13404 var language = lang ? lang : service.getCurrentLang();
13405 var trans = langCache.get(language);
13406
13407 if (!trans) {
13408 return i18nConstants.MISSING;
13409 }
13410
13411 var paths = path.split('.');
13412 var current = trans;
13413
13414 for (var i = 0; i < paths.length; ++i) {
13415 if (current[paths[i]] === undefined || current[paths[i]] === null) {
13416 return i18nConstants.MISSING;
13417 } else {
13418 current = current[paths[i]];
13419 }
13420 }
13421
13422 return current;
13423
13424 },
13425
13426 /**
13427 * @ngdoc service
13428 * @name setCurrentLang
13429 * @methodOf ui.grid.i18n.service:i18nService
13430 * @description sets the current language to use in the application
13431 * $broadcasts the Update_Event on the $rootScope
13432 * @param {string} lang to set
13433 * @example
13434 * <pre>
13435 * i18nService.setCurrentLang('fr');
13436 * </pre>
13437 */
13438
13439 setCurrentLang: function (lang) {
13440 if (lang) {
13441 langCache.setCurrent(lang);
13442 $rootScope.$broadcast(i18nConstants.UPDATE_EVENT);
13443 }
13444 },
13445
13446 /**
13447 * @ngdoc service
13448 * @name getCurrentLang
13449 * @methodOf ui.grid.i18n.service:i18nService
13450 * @description returns the current language used in the application
13451 */
13452 getCurrentLang: function () {
13453 var lang = langCache.getCurrentLang();
13454 if (!lang) {
13455 lang = i18nConstants.DEFAULT_LANG;
13456 langCache.setCurrent(lang);
13457 }
13458 return lang;
13459 }
13460
13461 };
13462
13463 return service;
13464
13465 }]);
13466
13467 var localeDirective = function (i18nService, i18nConstants) {
13468 return {
13469 compile: function () {
13470 return {
13471 pre: function ($scope, $elm, $attrs) {
13472 var alias = i18nConstants.LOCALE_DIRECTIVE_ALIAS;
13473 // check for watchable property
13474 var lang = $scope.$eval($attrs[alias]);
13475 if (lang) {
13476 $scope.$watch($attrs[alias], function () {
13477 i18nService.setCurrentLang(lang);
13478 });
13479 } else if ($attrs.$$observers) {
13480 $attrs.$observe(alias, function () {
13481 i18nService.setCurrentLang($attrs[alias] || i18nConstants.DEFAULT_LANG);
13482 });
13483 }
13484 }
13485 };
13486 }
13487 };
13488 };
13489
13490 module.directive('uiI18n', ['i18nService', 'i18nConstants', localeDirective]);
13491
13492 // directive syntax
13493 var uitDirective = function ($parse, i18nService, i18nConstants) {
13494 return {
13495 restrict: 'EA',
13496 compile: function () {
13497 return {
13498 pre: function ($scope, $elm, $attrs) {
13499 var alias1 = DIRECTIVE_ALIASES[0],
13500 alias2 = DIRECTIVE_ALIASES[1];
13501 var token = $attrs[alias1] || $attrs[alias2] || $elm.html();
13502 var missing = i18nConstants.MISSING + token;
13503 var observer;
13504 if ($attrs.$$observers) {
13505 var prop = $attrs[alias1] ? alias1 : alias2;
13506 observer = $attrs.$observe(prop, function (result) {
13507 if (result) {
13508 $elm.html($parse(result)(i18nService.getCurrentLang()) || missing);
13509 }
13510 });
13511 }
13512 var getter = $parse(token);
13513 var listener = $scope.$on(i18nConstants.UPDATE_EVENT, function (evt) {
13514 if (observer) {
13515 observer($attrs[alias1] || $attrs[alias2]);
13516 } else {
13517 // set text based on i18n current language
13518 $elm.html(getter(i18nService.get()) || missing);
13519 }
13520 });
13521 $scope.$on('$destroy', listener);
13522
13523 $elm.html(getter(i18nService.get()) || missing);
13524 }
13525 };
13526 }
13527 };
13528 };
13529
13530 angular.forEach( DIRECTIVE_ALIASES, function ( alias ) {
13531 module.directive( alias, ['$parse', 'i18nService', 'i18nConstants', uitDirective] );
13532 } );
13533
13534 // optional filter syntax
13535 var uitFilter = function ($parse, i18nService, i18nConstants) {
13536 return function (data) {
13537 var getter = $parse(data);
13538 // set text based on i18n current language
13539 return getter(i18nService.get()) || i18nConstants.MISSING + data;
13540 };
13541 };
13542
13543 angular.forEach( FILTER_ALIASES, function ( alias ) {
13544 module.filter( alias, ['$parse', 'i18nService', 'i18nConstants', uitFilter] );
13545 } );
13546
13547
13548 })();
13549 (function() {
13550 angular.module('ui.grid').config(['$provide', function($provide) {
13551 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13552 $delegate.add('zh-cn', {
13553 headerCell: {
13554 aria: {
13555 defaultFilterLabel: '列过滤器',
13556 removeFilter: '移除过滤器',
13557 columnMenuButtonLabel: '列菜单'
13558 },
13559 priority: '优先级:',
13560 filterLabel: "列过滤器: "
13561 },
13562 aggregate: {
13563 label: '行'
13564 },
13565 groupPanel: {
13566 description: '拖曳表头到此处进行分组'
13567 },
13568 search: {
13569 placeholder: '查找',
13570 showingItems: '已显示行数:',
13571 selectedItems: '已选择行数:',
13572 totalItems: '总行数:',
13573 size: '每页显示行数:',
13574 first: '首页',
13575 next: '下一页',
13576 previous: '上一页',
13577 last: '末页'
13578 },
13579 menu: {
13580 text: '选择列:'
13581 },
13582 sort: {
13583 ascending: '升序',
13584 descending: '降序',
13585 none: '无序',
13586 remove: '取消排序'
13587 },
13588 column: {
13589 hide: '隐藏列'
13590 },
13591 aggregation: {
13592 count: '计数:',
13593 sum: '求和:',
13594 avg: '均值:',
13595 min: '最小值:',
13596 max: '最大值:'
13597 },
13598 pinning: {
13599 pinLeft: '左侧固定',
13600 pinRight: '右侧固定',
13601 unpin: '取消固定'
13602 },
13603 columnMenu: {
13604 close: '关闭'
13605 },
13606 gridMenu: {
13607 aria: {
13608 buttonLabel: '表格菜单'
13609 },
13610 columns: '列:',
13611 importerTitle: '导入文件',
13612 exporterAllAsCsv: '导出全部数据到CSV',
13613 exporterVisibleAsCsv: '导出可见数据到CSV',
13614 exporterSelectedAsCsv: '导出已选数据到CSV',
13615 exporterAllAsPdf: '导出全部数据到PDF',
13616 exporterVisibleAsPdf: '导出可见数据到PDF',
13617 exporterSelectedAsPdf: '导出已选数据到PDF',
13618 clearAllFilters: '清除所有过滤器'
13619 },
13620 importer: {
13621 noHeaders: '无法获取列名,确定文件包含表头?',
13622 noObjects: '无法获取数据,确定文件包含数据?',
13623 invalidCsv: '无法处理文件,确定是合法的CSV文件?',
13624 invalidJson: '无法处理文件,确定是合法的JSON文件?',
13625 jsonNotArray: '导入的文件不是JSON数组!'
13626 },
13627 pagination: {
13628 aria: {
13629 pageToFirst: '第一页',
13630 pageBack: '上一页',
13631 pageSelected: '当前页',
13632 pageForward: '下一页',
13633 pageToLast: '最后一页'
13634 },
13635 sizes: '行每页',
13636 totalItems: '行',
13637 through: '至',
13638 of: '共'
13639 },
13640 grouping: {
13641 group: '分组',
13642 ungroup: '取消分组',
13643 aggregate_count: '合计: 计数',
13644 aggregate_sum: '合计: 求和',
13645 aggregate_max: '合计: 最大',
13646 aggregate_min: '合计: 最小',
13647 aggregate_avg: '合计: 平均',
13648 aggregate_remove: '合计: 移除'
13649 }
13650 });
13651 return $delegate;
13652 }]);
13653 }]);
13654 })();
13655
13656 (function() {
13657 angular.module('ui.grid').config(['$provide', function($provide) {
13658 $provide.decorator('i18nService', ['$delegate', function($delegate) {
13659 $delegate.add('zh-tw', {
13660 aggregate: {
13661 label: '行'
13662 },
13663 groupPanel: {
13664 description: '拖曳表頭到此處進行分組'
13665 },
13666 search: {
13667 placeholder: '查找',
13668 showingItems: '已顯示行數:',
13669 selectedItems: '已選擇行數:',
13670 totalItems: '總行數:',
13671 size: '每頁顯示行數:',
13672 first: '首頁',
13673 next: '下壹頁',
13674 previous: '上壹頁',
13675 last: '末頁'
13676 },
13677 menu: {
13678 text: '選擇列:'
13679 },
13680 sort: {
13681 ascending: '升序',
13682 descending: '降序',
13683 remove: '取消排序'
13684 },
13685 column: {
13686 hide: '隱藏列'
13687 },
13688 aggregation: {
13689 count: '計數:',
13690 sum: '求和:',
13691 avg: '均值:',
13692 min: '最小值:',
13693 max: '最大值:'
13694 },
13695 pinning: {
13696 pinLeft: '左側固定',
13697 pinRight: '右側固定',
13698 unpin: '取消固定'
13699 },
13700 gridMenu: {
13701 columns: '列:',
13702 importerTitle: '導入文件',
13703 exporterAllAsCsv: '導出全部數據到CSV',
13704 exporterVisibleAsCsv: '導出可見數據到CSV',
13705 exporterSelectedAsCsv: '導出已選數據到CSV',
13706 exporterAllAsPdf: '導出全部數據到PDF',
13707 exporterVisibleAsPdf: '導出可見數據到PDF',
13708 exporterSelectedAsPdf: '導出已選數據到PDF',
13709 clearAllFilters: '清除所有过滤器'
13710 },
13711 importer: {
13712 noHeaders: '無法獲取列名,確定文件包含表頭?',
13713 noObjects: '無法獲取數據,確定文件包含數據?',
13714 invalidCsv: '無法處理文件,確定是合法的CSV文件?',
13715 invalidJson: '無法處理文件,確定是合法的JSON文件?',
13716 jsonNotArray: '導入的文件不是JSON數組!'
13717 },
13718 pagination: {
13719 sizes: '行每頁',
13720 totalItems: '行'
13721 }
13722 });
13723 return $delegate;
13724 }]);
13725 }]);
13726 })();
13727
13728 (function() {
13729 'use strict';
13730 /**
13731 * @ngdoc overview
13732 * @name ui.grid.autoResize
13733 *
13734 * @description
13735 *
13736 * #ui.grid.autoResize
13737 *
13738 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
13739 *
13740 * This module provides auto-resizing functionality to UI-Grid.
13741 */
13742 var module = angular.module('ui.grid.autoResize', ['ui.grid']);
13743
13744
13745 module.directive('uiGridAutoResize', ['$timeout', 'gridUtil', function ($timeout, gridUtil) {
13746 return {
13747 require: 'uiGrid',
13748 scope: false,
13749 link: function ($scope, $elm, $attrs, uiGridCtrl) {
13750 var prevGridWidth, prevGridHeight;
13751
13752 function getDimensions() {
13753 prevGridHeight = gridUtil.elementHeight($elm);
13754 prevGridWidth = gridUtil.elementWidth($elm);
13755 }
13756
13757 // Initialize the dimensions
13758 getDimensions();
13759
13760 var resizeTimeoutId;
13761 function startTimeout() {
13762 clearTimeout(resizeTimeoutId);
13763
13764 resizeTimeoutId = setTimeout(function () {
13765 var newGridHeight = gridUtil.elementHeight($elm);
13766 var newGridWidth = gridUtil.elementWidth($elm);
13767
13768 if (newGridHeight !== prevGridHeight || newGridWidth !== prevGridWidth) {
13769 uiGridCtrl.grid.gridHeight = newGridHeight;
13770 uiGridCtrl.grid.gridWidth = newGridWidth;
13771
13772 $scope.$apply(function () {
13773 uiGridCtrl.grid.refresh()
13774 .then(function () {
13775 getDimensions();
13776
13777 startTimeout();
13778 });
13779 });
13780 }
13781 else {
13782 startTimeout();
13783 }
13784 }, 250);
13785 }
13786
13787 startTimeout();
13788
13789 $scope.$on('$destroy', function() {
13790 clearTimeout(resizeTimeoutId);
13791 });
13792 }
13793 };
13794 }]);
13795 })();
13796
13797 (function () {
13798 'use strict';
13799
13800 /**
13801 * @ngdoc overview
13802 * @name ui.grid.cellNav
13803 *
13804 * @description
13805
13806 #ui.grid.cellNav
13807
13808 <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
13809
13810 This module provides auto-resizing functionality to UI-Grid.
13811 */
13812 var module = angular.module('ui.grid.cellNav', ['ui.grid']);
13813
13814 /**
13815 * @ngdoc object
13816 * @name ui.grid.cellNav.constant:uiGridCellNavConstants
13817 *
13818 * @description constants available in cellNav
13819 */
13820 module.constant('uiGridCellNavConstants', {
13821 FEATURE_NAME: 'gridCellNav',
13822 CELL_NAV_EVENT: 'cellNav',
13823 direction: {LEFT: 0, RIGHT: 1, UP: 2, DOWN: 3, PG_UP: 4, PG_DOWN: 5},
13824 EVENT_TYPE: {
13825 KEYDOWN: 0,
13826 CLICK: 1,
13827 CLEAR: 2
13828 }
13829 });
13830
13831
13832 module.factory('uiGridCellNavFactory', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', 'GridRowColumn', '$q',
13833 function (gridUtil, uiGridConstants, uiGridCellNavConstants, GridRowColumn, $q) {
13834 /**
13835 * @ngdoc object
13836 * @name ui.grid.cellNav.object:CellNav
13837 * @description returns a CellNav prototype function
13838 * @param {object} rowContainer container for rows
13839 * @param {object} colContainer parent column container
13840 * @param {object} leftColContainer column container to the left of parent
13841 * @param {object} rightColContainer column container to the right of parent
13842 */
13843 var UiGridCellNav = function UiGridCellNav(rowContainer, colContainer, leftColContainer, rightColContainer) {
13844 this.rows = rowContainer.visibleRowCache;
13845 this.columns = colContainer.visibleColumnCache;
13846 this.leftColumns = leftColContainer ? leftColContainer.visibleColumnCache : [];
13847 this.rightColumns = rightColContainer ? rightColContainer.visibleColumnCache : [];
13848 this.bodyContainer = rowContainer;
13849 };
13850
13851 /** returns focusable columns of all containers */
13852 UiGridCellNav.prototype.getFocusableCols = function () {
13853 var allColumns = this.leftColumns.concat(this.columns, this.rightColumns);
13854
13855 return allColumns.filter(function (col) {
13856 return col.colDef.allowCellFocus;
13857 });
13858 };
13859
13860 /**
13861 * @ngdoc object
13862 * @name ui.grid.cellNav.api:GridRow
13863 *
13864 * @description GridRow settings for cellNav feature, these are available to be
13865 * set only internally (for example, by other features)
13866 */
13867
13868 /**
13869 * @ngdoc object
13870 * @name allowCellFocus
13871 * @propertyOf ui.grid.cellNav.api:GridRow
13872 * @description Enable focus on a cell within this row. If set to false then no cells
13873 * in this row can be focused - group header rows as an example would set this to false.
13874 * <br/>Defaults to true
13875 */
13876 /** returns focusable rows */
13877 UiGridCellNav.prototype.getFocusableRows = function () {
13878 return this.rows.filter(function(row) {
13879 return row.allowCellFocus !== false;
13880 });
13881 };
13882
13883 UiGridCellNav.prototype.getNextRowCol = function (direction, curRow, curCol) {
13884 switch (direction) {
13885 case uiGridCellNavConstants.direction.LEFT:
13886 return this.getRowColLeft(curRow, curCol);
13887 case uiGridCellNavConstants.direction.RIGHT:
13888 return this.getRowColRight(curRow, curCol);
13889 case uiGridCellNavConstants.direction.UP:
13890 return this.getRowColUp(curRow, curCol);
13891 case uiGridCellNavConstants.direction.DOWN:
13892 return this.getRowColDown(curRow, curCol);
13893 case uiGridCellNavConstants.direction.PG_UP:
13894 return this.getRowColPageUp(curRow, curCol);
13895 case uiGridCellNavConstants.direction.PG_DOWN:
13896 return this.getRowColPageDown(curRow, curCol);
13897 }
13898
13899 };
13900
13901 UiGridCellNav.prototype.initializeSelection = function () {
13902 var focusableCols = this.getFocusableCols();
13903 var focusableRows = this.getFocusableRows();
13904 if (focusableCols.length === 0 || focusableRows.length === 0) {
13905 return null;
13906 }
13907
13908 var curRowIndex = 0;
13909 var curColIndex = 0;
13910 return new GridRowColumn(focusableRows[0], focusableCols[0]); //return same row
13911 };
13912
13913 UiGridCellNav.prototype.getRowColLeft = function (curRow, curCol) {
13914 var focusableCols = this.getFocusableCols();
13915 var focusableRows = this.getFocusableRows();
13916 var curColIndex = focusableCols.indexOf(curCol);
13917 var curRowIndex = focusableRows.indexOf(curRow);
13918
13919 //could not find column in focusable Columns so set it to 1
13920 if (curColIndex === -1) {
13921 curColIndex = 1;
13922 }
13923
13924 var nextColIndex = curColIndex === 0 ? focusableCols.length - 1 : curColIndex - 1;
13925
13926 //get column to left
13927 if (nextColIndex > curColIndex) {
13928 // On the first row
13929 // if (curRowIndex === 0 && curColIndex === 0) {
13930 // return null;
13931 // }
13932 if (curRowIndex === 0) {
13933 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
13934 }
13935 else {
13936 //up one row and far right column
13937 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[nextColIndex]);
13938 }
13939 }
13940 else {
13941 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
13942 }
13943 };
13944
13945
13946
13947 UiGridCellNav.prototype.getRowColRight = function (curRow, curCol) {
13948 var focusableCols = this.getFocusableCols();
13949 var focusableRows = this.getFocusableRows();
13950 var curColIndex = focusableCols.indexOf(curCol);
13951 var curRowIndex = focusableRows.indexOf(curRow);
13952
13953 //could not find column in focusable Columns so set it to 0
13954 if (curColIndex === -1) {
13955 curColIndex = 0;
13956 }
13957 var nextColIndex = curColIndex === focusableCols.length - 1 ? 0 : curColIndex + 1;
13958
13959 if (nextColIndex < curColIndex) {
13960 if (curRowIndex === focusableRows.length - 1) {
13961 return new GridRowColumn(curRow, focusableCols[nextColIndex]); //return same row
13962 }
13963 else {
13964 //down one row and far left column
13965 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[nextColIndex]);
13966 }
13967 }
13968 else {
13969 return new GridRowColumn(curRow, focusableCols[nextColIndex]);
13970 }
13971 };
13972
13973 UiGridCellNav.prototype.getRowColDown = function (curRow, curCol) {
13974 var focusableCols = this.getFocusableCols();
13975 var focusableRows = this.getFocusableRows();
13976 var curColIndex = focusableCols.indexOf(curCol);
13977 var curRowIndex = focusableRows.indexOf(curRow);
13978
13979 //could not find column in focusable Columns so set it to 0
13980 if (curColIndex === -1) {
13981 curColIndex = 0;
13982 }
13983
13984 if (curRowIndex === focusableRows.length - 1) {
13985 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
13986 }
13987 else {
13988 //down one row
13989 return new GridRowColumn(focusableRows[curRowIndex + 1], focusableCols[curColIndex]);
13990 }
13991 };
13992
13993 UiGridCellNav.prototype.getRowColPageDown = function (curRow, curCol) {
13994 var focusableCols = this.getFocusableCols();
13995 var focusableRows = this.getFocusableRows();
13996 var curColIndex = focusableCols.indexOf(curCol);
13997 var curRowIndex = focusableRows.indexOf(curRow);
13998
13999 //could not find column in focusable Columns so set it to 0
14000 if (curColIndex === -1) {
14001 curColIndex = 0;
14002 }
14003
14004 var pageSize = this.bodyContainer.minRowsToRender();
14005 if (curRowIndex >= focusableRows.length - pageSize) {
14006 return new GridRowColumn(focusableRows[focusableRows.length - 1], focusableCols[curColIndex]); //return last row
14007 }
14008 else {
14009 //down one page
14010 return new GridRowColumn(focusableRows[curRowIndex + pageSize], focusableCols[curColIndex]);
14011 }
14012 };
14013
14014 UiGridCellNav.prototype.getRowColUp = function (curRow, curCol) {
14015 var focusableCols = this.getFocusableCols();
14016 var focusableRows = this.getFocusableRows();
14017 var curColIndex = focusableCols.indexOf(curCol);
14018 var curRowIndex = focusableRows.indexOf(curRow);
14019
14020 //could not find column in focusable Columns so set it to 0
14021 if (curColIndex === -1) {
14022 curColIndex = 0;
14023 }
14024
14025 if (curRowIndex === 0) {
14026 return new GridRowColumn(curRow, focusableCols[curColIndex]); //return same row
14027 }
14028 else {
14029 //up one row
14030 return new GridRowColumn(focusableRows[curRowIndex - 1], focusableCols[curColIndex]);
14031 }
14032 };
14033
14034 UiGridCellNav.prototype.getRowColPageUp = function (curRow, curCol) {
14035 var focusableCols = this.getFocusableCols();
14036 var focusableRows = this.getFocusableRows();
14037 var curColIndex = focusableCols.indexOf(curCol);
14038 var curRowIndex = focusableRows.indexOf(curRow);
14039
14040 //could not find column in focusable Columns so set it to 0
14041 if (curColIndex === -1) {
14042 curColIndex = 0;
14043 }
14044
14045 var pageSize = this.bodyContainer.minRowsToRender();
14046 if (curRowIndex - pageSize < 0) {
14047 return new GridRowColumn(focusableRows[0], focusableCols[curColIndex]); //return first row
14048 }
14049 else {
14050 //up one page
14051 return new GridRowColumn(focusableRows[curRowIndex - pageSize], focusableCols[curColIndex]);
14052 }
14053 };
14054 return UiGridCellNav;
14055 }]);
14056
14057 /**
14058 * @ngdoc service
14059 * @name ui.grid.cellNav.service:uiGridCellNavService
14060 *
14061 * @description Services for cell navigation features. If you don't like the key maps we use,
14062 * or the direction cells navigation, override with a service decorator (see angular docs)
14063 */
14064 module.service('uiGridCellNavService', ['gridUtil', 'uiGridConstants', 'uiGridCellNavConstants', '$q', 'uiGridCellNavFactory', 'GridRowColumn', 'ScrollEvent',
14065 function (gridUtil, uiGridConstants, uiGridCellNavConstants, $q, UiGridCellNav, GridRowColumn, ScrollEvent) {
14066
14067 var service = {
14068
14069 initializeGrid: function (grid) {
14070 grid.registerColumnBuilder(service.cellNavColumnBuilder);
14071
14072
14073 /**
14074 * @ngdoc object
14075 * @name ui.grid.cellNav:Grid.cellNav
14076 * @description cellNav properties added to grid class
14077 */
14078 grid.cellNav = {};
14079 grid.cellNav.lastRowCol = null;
14080 grid.cellNav.focusedCells = [];
14081
14082 service.defaultGridOptions(grid.options);
14083
14084 /**
14085 * @ngdoc object
14086 * @name ui.grid.cellNav.api:PublicApi
14087 *
14088 * @description Public Api for cellNav feature
14089 */
14090 var publicApi = {
14091 events: {
14092 cellNav: {
14093 /**
14094 * @ngdoc event
14095 * @name navigate
14096 * @eventOf ui.grid.cellNav.api:PublicApi
14097 * @description raised when the active cell is changed
14098 * <pre>
14099 * gridApi.cellNav.on.navigate(scope,function(newRowcol, oldRowCol){})
14100 * </pre>
14101 * @param {object} newRowCol new position
14102 * @param {object} oldRowCol old position
14103 */
14104 navigate: function (newRowCol, oldRowCol) {},
14105 /**
14106 * @ngdoc event
14107 * @name viewPortKeyDown
14108 * @eventOf ui.grid.cellNav.api:PublicApi
14109 * @description is raised when the viewPort receives a keyDown event. Cells never get focus in uiGrid
14110 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14111 * event whenever you need a keydown event on a cell
14112 * <br/>
14113 * @param {object} event keydown event
14114 * @param {object} rowCol current rowCol position
14115 */
14116 viewPortKeyDown: function (event, rowCol) {},
14117
14118 /**
14119 * @ngdoc event
14120 * @name viewPortKeyPress
14121 * @eventOf ui.grid.cellNav.api:PublicApi
14122 * @description is raised when the viewPort receives a keyPress event. Cells never get focus in uiGrid
14123 * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this
14124 * event whenever you need a keypress event on a cell
14125 * <br/>
14126 * @param {object} event keypress event
14127 * @param {object} rowCol current rowCol position
14128 */
14129 viewPortKeyPress: function (event, rowCol) {}
14130 }
14131 },
14132 methods: {
14133 cellNav: {
14134 /**
14135 * @ngdoc function
14136 * @name scrollToFocus
14137 * @methodOf ui.grid.cellNav.api:PublicApi
14138 * @description brings the specified row and column into view, and sets focus
14139 * to that cell
14140 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus
14141 * @param {object} colDef to make visible and set focus
14142 * @returns {promise} a promise that is resolved after any scrolling is finished
14143 */
14144 scrollToFocus: function (rowEntity, colDef) {
14145 return service.scrollToFocus(grid, rowEntity, colDef);
14146 },
14147
14148 /**
14149 * @ngdoc function
14150 * @name getFocusedCell
14151 * @methodOf ui.grid.cellNav.api:PublicApi
14152 * @description returns the current (or last if Grid does not have focus) focused row and column
14153 * <br> value is null if no selection has occurred
14154 */
14155 getFocusedCell: function () {
14156 return grid.cellNav.lastRowCol;
14157 },
14158
14159 /**
14160 * @ngdoc function
14161 * @name getCurrentSelection
14162 * @methodOf ui.grid.cellNav.api:PublicApi
14163 * @description returns an array containing the current selection
14164 * <br> array is empty if no selection has occurred
14165 */
14166 getCurrentSelection: function () {
14167 return grid.cellNav.focusedCells;
14168 },
14169
14170 /**
14171 * @ngdoc function
14172 * @name rowColSelectIndex
14173 * @methodOf ui.grid.cellNav.api:PublicApi
14174 * @description returns the index in the order in which the GridRowColumn was selected, returns -1 if the GridRowColumn
14175 * isn't selected
14176 * @param {object} rowCol the rowCol to evaluate
14177 */
14178 rowColSelectIndex: function (rowCol) {
14179 //return gridUtil.arrayContainsObjectWithProperty(grid.cellNav.focusedCells, 'col.uid', rowCol.col.uid) &&
14180 var index = -1;
14181 for (var i = 0; i < grid.cellNav.focusedCells.length; i++) {
14182 if (grid.cellNav.focusedCells[i].col.uid === rowCol.col.uid &&
14183 grid.cellNav.focusedCells[i].row.uid === rowCol.row.uid) {
14184 index = i;
14185 break;
14186 }
14187 }
14188 return index;
14189 }
14190 }
14191 }
14192 };
14193
14194 grid.api.registerEventsFromObject(publicApi.events);
14195
14196 grid.api.registerMethodsFromObject(publicApi.methods);
14197
14198 },
14199
14200 defaultGridOptions: function (gridOptions) {
14201 /**
14202 * @ngdoc object
14203 * @name ui.grid.cellNav.api:GridOptions
14204 *
14205 * @description GridOptions for cellNav feature, these are available to be
14206 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
14207 */
14208
14209 /**
14210 * @ngdoc object
14211 * @name modifierKeysToMultiSelectCells
14212 * @propertyOf ui.grid.cellNav.api:GridOptions
14213 * @description Enable multiple cell selection only when using the ctrlKey or shiftKey.
14214 * <br/>Defaults to false
14215 */
14216 gridOptions.modifierKeysToMultiSelectCells = gridOptions.modifierKeysToMultiSelectCells === true;
14217
14218 },
14219
14220 /**
14221 * @ngdoc service
14222 * @name decorateRenderContainers
14223 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14224 * @description decorates grid renderContainers with cellNav functions
14225 */
14226 decorateRenderContainers: function (grid) {
14227
14228 var rightContainer = grid.hasRightContainer() ? grid.renderContainers.right : null;
14229 var leftContainer = grid.hasLeftContainer() ? grid.renderContainers.left : null;
14230
14231 if (leftContainer !== null) {
14232 grid.renderContainers.left.cellNav = new UiGridCellNav(grid.renderContainers.body, leftContainer, rightContainer, grid.renderContainers.body);
14233 }
14234 if (rightContainer !== null) {
14235 grid.renderContainers.right.cellNav = new UiGridCellNav(grid.renderContainers.body, rightContainer, grid.renderContainers.body, leftContainer);
14236 }
14237
14238 grid.renderContainers.body.cellNav = new UiGridCellNav(grid.renderContainers.body, grid.renderContainers.body, leftContainer, rightContainer);
14239 },
14240
14241 /**
14242 * @ngdoc service
14243 * @name getDirection
14244 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14245 * @description determines which direction to for a given keyDown event
14246 * @returns {uiGridCellNavConstants.direction} direction
14247 */
14248 getDirection: function (evt) {
14249 if (evt.keyCode === uiGridConstants.keymap.LEFT ||
14250 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey)) {
14251 return uiGridCellNavConstants.direction.LEFT;
14252 }
14253 if (evt.keyCode === uiGridConstants.keymap.RIGHT ||
14254 evt.keyCode === uiGridConstants.keymap.TAB) {
14255 return uiGridCellNavConstants.direction.RIGHT;
14256 }
14257
14258 if (evt.keyCode === uiGridConstants.keymap.UP ||
14259 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ) {
14260 return uiGridCellNavConstants.direction.UP;
14261 }
14262
14263 if (evt.keyCode === uiGridConstants.keymap.PG_UP){
14264 return uiGridCellNavConstants.direction.PG_UP;
14265 }
14266
14267 if (evt.keyCode === uiGridConstants.keymap.DOWN ||
14268 evt.keyCode === uiGridConstants.keymap.ENTER && !(evt.ctrlKey || evt.altKey)) {
14269 return uiGridCellNavConstants.direction.DOWN;
14270 }
14271
14272 if (evt.keyCode === uiGridConstants.keymap.PG_DOWN){
14273 return uiGridCellNavConstants.direction.PG_DOWN;
14274 }
14275
14276 return null;
14277 },
14278
14279 /**
14280 * @ngdoc service
14281 * @name cellNavColumnBuilder
14282 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14283 * @description columnBuilder function that adds cell navigation properties to grid column
14284 * @returns {promise} promise that will load any needed templates when resolved
14285 */
14286 cellNavColumnBuilder: function (colDef, col, gridOptions) {
14287 var promises = [];
14288
14289 /**
14290 * @ngdoc object
14291 * @name ui.grid.cellNav.api:ColumnDef
14292 *
14293 * @description Column Definitions for cellNav feature, these are available to be
14294 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
14295 */
14296
14297 /**
14298 * @ngdoc object
14299 * @name allowCellFocus
14300 * @propertyOf ui.grid.cellNav.api:ColumnDef
14301 * @description Enable focus on a cell within this column.
14302 * <br/>Defaults to true
14303 */
14304 colDef.allowCellFocus = colDef.allowCellFocus === undefined ? true : colDef.allowCellFocus;
14305
14306 return $q.all(promises);
14307 },
14308
14309 /**
14310 * @ngdoc method
14311 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14312 * @name scrollToFocus
14313 * @description Scroll the grid such that the specified
14314 * row and column is in view, and set focus to the cell in that row and column
14315 * @param {Grid} grid the grid you'd like to act upon, usually available
14316 * from gridApi.grid
14317 * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus to
14318 * @param {object} colDef to make visible and set focus to
14319 * @returns {promise} a promise that is resolved after any scrolling is finished
14320 */
14321 scrollToFocus: function (grid, rowEntity, colDef) {
14322 var gridRow = null, gridCol = null;
14323
14324 if (typeof(rowEntity) !== 'undefined' && rowEntity !== null) {
14325 gridRow = grid.getRow(rowEntity);
14326 }
14327
14328 if (typeof(colDef) !== 'undefined' && colDef !== null) {
14329 gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field);
14330 }
14331 return grid.api.core.scrollToIfNecessary(gridRow, gridCol).then(function () {
14332 var rowCol = { row: gridRow, col: gridCol };
14333
14334 // Broadcast the navigation
14335 if (gridRow !== null && gridCol !== null) {
14336 grid.cellNav.broadcastCellNav(rowCol);
14337 }
14338 });
14339
14340
14341
14342 },
14343
14344
14345 /**
14346 * @ngdoc method
14347 * @methodOf ui.grid.cellNav.service:uiGridCellNavService
14348 * @name getLeftWidth
14349 * @description Get the current drawn width of the columns in the
14350 * grid up to the numbered column, and add an apportionment for the
14351 * column that we're on. So if we are on column 0, we want to scroll
14352 * 0% (i.e. exclude this column from calc). If we're on the last column
14353 * we want to scroll to 100% (i.e. include this column in the calc). So
14354 * we include (thisColIndex / totalNumberCols) % of this column width
14355 * @param {Grid} grid the grid you'd like to act upon, usually available
14356 * from gridApi.grid
14357 * @param {gridCol} upToCol the column to total up to and including
14358 */
14359 getLeftWidth: function (grid, upToCol) {
14360 var width = 0;
14361
14362 if (!upToCol) {
14363 return width;
14364 }
14365
14366 var lastIndex = grid.renderContainers.body.visibleColumnCache.indexOf( upToCol );
14367
14368 // total column widths up-to but not including the passed in column
14369 grid.renderContainers.body.visibleColumnCache.forEach( function( col, index ) {
14370 if ( index < lastIndex ){
14371 width += col.drawnWidth;
14372 }
14373 });
14374
14375 // pro-rata the final column based on % of total columns.
14376 var percentage = lastIndex === 0 ? 0 : (lastIndex + 1) / grid.renderContainers.body.visibleColumnCache.length;
14377 width += upToCol.drawnWidth * percentage;
14378
14379 return width;
14380 }
14381 };
14382
14383 return service;
14384 }]);
14385
14386 /**
14387 * @ngdoc directive
14388 * @name ui.grid.cellNav.directive:uiCellNav
14389 * @element div
14390 * @restrict EA
14391 *
14392 * @description Adds cell navigation features to the grid columns
14393 *
14394 * @example
14395 <example module="app">
14396 <file name="app.js">
14397 var app = angular.module('app', ['ui.grid', 'ui.grid.cellNav']);
14398
14399 app.controller('MainCtrl', ['$scope', function ($scope) {
14400 $scope.data = [
14401 { name: 'Bob', title: 'CEO' },
14402 { name: 'Frank', title: 'Lowly Developer' }
14403 ];
14404
14405 $scope.columnDefs = [
14406 {name: 'name'},
14407 {name: 'title'}
14408 ];
14409 }]);
14410 </file>
14411 <file name="index.html">
14412 <div ng-controller="MainCtrl">
14413 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-cellnav></div>
14414 </div>
14415 </file>
14416 </example>
14417 */
14418 module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn', '$timeout', '$compile',
14419 function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, GridRowColumn, $timeout, $compile) {
14420 return {
14421 replace: true,
14422 priority: -150,
14423 require: '^uiGrid',
14424 scope: false,
14425 controller: function () {},
14426 compile: function () {
14427 return {
14428 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
14429 var _scope = $scope;
14430
14431 var grid = uiGridCtrl.grid;
14432 uiGridCellNavService.initializeGrid(grid);
14433
14434 uiGridCtrl.cellNav = {};
14435
14436 //Ensure that the object has all of the methods we expect it to
14437 uiGridCtrl.cellNav.makeRowCol = function (obj) {
14438 if (!(obj instanceof GridRowColumn)) {
14439 obj = new GridRowColumn(obj.row, obj.col);
14440 }
14441 return obj;
14442 };
14443
14444 uiGridCtrl.cellNav.getActiveCell = function () {
14445 var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus');
14446 if (elms.length > 0){
14447 return elms[0];
14448 }
14449
14450 return undefined;
14451 };
14452
14453 uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown, originEvt) {
14454 modifierDown = !(modifierDown === undefined || !modifierDown);
14455
14456 newRowCol = uiGridCtrl.cellNav.makeRowCol(newRowCol);
14457
14458 uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown, originEvt);
14459 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT, newRowCol, modifierDown, originEvt);
14460 };
14461
14462 uiGridCtrl.cellNav.clearFocus = grid.cellNav.clearFocus = function () {
14463 grid.cellNav.focusedCells = [];
14464 _scope.$broadcast(uiGridCellNavConstants.CELL_NAV_EVENT);
14465 };
14466
14467 uiGridCtrl.cellNav.broadcastFocus = function (rowCol, modifierDown, originEvt) {
14468 modifierDown = !(modifierDown === undefined || !modifierDown);
14469
14470 rowCol = uiGridCtrl.cellNav.makeRowCol(rowCol);
14471
14472 var row = rowCol.row,
14473 col = rowCol.col;
14474
14475 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
14476
14477 if (grid.cellNav.lastRowCol === null || rowColSelectIndex === -1) {
14478 var newRowCol = new GridRowColumn(row, col);
14479
14480 grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol);
14481 grid.cellNav.lastRowCol = newRowCol;
14482 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown) {
14483 grid.cellNav.focusedCells.push(rowCol);
14484 } else {
14485 grid.cellNav.focusedCells = [rowCol];
14486 }
14487 } else if (grid.options.modifierKeysToMultiSelectCells && modifierDown &&
14488 rowColSelectIndex >= 0) {
14489
14490 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14491 }
14492 };
14493
14494 uiGridCtrl.cellNav.handleKeyDown = function (evt) {
14495 var direction = uiGridCellNavService.getDirection(evt);
14496 if (direction === null) {
14497 return null;
14498 }
14499
14500 var containerId = 'body';
14501 if (evt.uiGridTargetRenderContainerId) {
14502 containerId = evt.uiGridTargetRenderContainerId;
14503 }
14504
14505 // Get the last-focused row+col combo
14506 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14507 if (lastRowCol) {
14508 // Figure out which new row+combo we're navigating to
14509 var rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(direction, lastRowCol.row, lastRowCol.col);
14510 var focusableCols = uiGridCtrl.grid.renderContainers[containerId].cellNav.getFocusableCols();
14511 var rowColSelectIndex = uiGridCtrl.grid.api.cellNav.rowColSelectIndex(rowCol);
14512 // Shift+tab on top-left cell should exit cellnav on render container
14513 if (
14514 // Navigating left
14515 direction === uiGridCellNavConstants.direction.LEFT &&
14516 // New col is last col (i.e. wrap around)
14517 rowCol.col === focusableCols[focusableCols.length - 1] &&
14518 // Staying on same row, which means we're at first row
14519 rowCol.row === lastRowCol.row &&
14520 evt.keyCode === uiGridConstants.keymap.TAB &&
14521 evt.shiftKey
14522 ) {
14523 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14524 uiGridCtrl.cellNav.clearFocus();
14525 return true;
14526 }
14527 // Tab on bottom-right cell should exit cellnav on render container
14528 else if (
14529 direction === uiGridCellNavConstants.direction.RIGHT &&
14530 // New col is first col (i.e. wrap around)
14531 rowCol.col === focusableCols[0] &&
14532 // Staying on same row, which means we're at first row
14533 rowCol.row === lastRowCol.row &&
14534 evt.keyCode === uiGridConstants.keymap.TAB &&
14535 !evt.shiftKey
14536 ) {
14537 grid.cellNav.focusedCells.splice(rowColSelectIndex, 1);
14538 uiGridCtrl.cellNav.clearFocus();
14539 return true;
14540 }
14541
14542 // Scroll to the new cell, if it's not completely visible within the render container's viewport
14543 grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () {
14544 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
14545 });
14546
14547
14548 evt.stopPropagation();
14549 evt.preventDefault();
14550
14551 return false;
14552 }
14553 };
14554 },
14555 post: function ($scope, $elm, $attrs, uiGridCtrl) {
14556 var _scope = $scope;
14557 var grid = uiGridCtrl.grid;
14558
14559 function addAriaLiveRegion(){
14560 // Thanks to google docs for the inspiration behind how to do this
14561 // XXX: Why is this entire mess nessasary?
14562 // Because browsers take a lot of coercing to get them to read out live regions
14563 //http://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
14564 var ariaNotifierDomElt = '<div ' +
14565 'id="' + grid.id +'-aria-speakable" ' +
14566 'class="ui-grid-a11y-ariascreenreader-speakable ui-grid-offscreen" ' +
14567 'aria-live="assertive" ' +
14568 'role="region" ' +
14569 'aria-atomic="true" ' +
14570 'aria-hidden="false" ' +
14571 'aria-relevant="additions" ' +
14572 '>' +
14573 '&nbsp;' +
14574 '</div>';
14575
14576 var ariaNotifier = $compile(ariaNotifierDomElt)($scope);
14577 $elm.prepend(ariaNotifier);
14578 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown, originEvt) {
14579 /*
14580 * If the cell nav event was because of a focus event then we don't want to
14581 * change the notifier text.
14582 * Reasoning: Voice Over fires a focus events when moving arround the grid.
14583 * If the screen reader is handing the grid nav properly then we don't need to
14584 * use the alert to notify the user of the movement.
14585 * In all other cases we do want a notification event.
14586 */
14587 if (originEvt && originEvt.type === 'focus'){return;}
14588
14589 function setNotifyText(text){
14590 if (text === ariaNotifier.text()){return;}
14591 ariaNotifier[0].style.clip = 'rect(0px,0px,0px,0px)';
14592 /*
14593 * This is how google docs handles clearing the div. Seems to work better than setting the text of the div to ''
14594 */
14595 ariaNotifier[0].innerHTML = "";
14596 ariaNotifier[0].style.visibility = 'hidden';
14597 ariaNotifier[0].style.visibility = 'visible';
14598 if (text !== ''){
14599 ariaNotifier[0].style.clip = 'auto';
14600 /*
14601 * The space after the text is something that google docs does.
14602 */
14603 ariaNotifier[0].appendChild(document.createTextNode(text + " "));
14604 ariaNotifier[0].style.visibility = 'hidden';
14605 ariaNotifier[0].style.visibility = 'visible';
14606 }
14607 }
14608
14609 var values = [];
14610 var currentSelection = grid.api.cellNav.getCurrentSelection();
14611 for (var i = 0; i < currentSelection.length; i++) {
14612 values.push(currentSelection[i].getIntersectionValueFiltered());
14613 }
14614 var cellText = values.toString();
14615 setNotifyText(cellText);
14616
14617 });
14618 }
14619 addAriaLiveRegion();
14620 }
14621 };
14622 }
14623 };
14624 }]);
14625
14626 module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', '$compile','uiGridCellNavConstants',
14627 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, $compile, uiGridCellNavConstants) {
14628 return {
14629 replace: true,
14630 priority: -99999, //this needs to run very last
14631 require: ['^uiGrid', 'uiGridRenderContainer', '?^uiGridCellnav'],
14632 scope: false,
14633 compile: function () {
14634 return {
14635 post: function ($scope, $elm, $attrs, controllers) {
14636 var uiGridCtrl = controllers[0],
14637 renderContainerCtrl = controllers[1],
14638 uiGridCellnavCtrl = controllers[2];
14639
14640 // Skip attaching cell-nav specific logic if the directive is not attached above us
14641 if (!uiGridCtrl.grid.api.cellNav) { return; }
14642
14643 var containerId = renderContainerCtrl.containerId;
14644
14645 var grid = uiGridCtrl.grid;
14646
14647 //run each time a render container is created
14648 uiGridCellNavService.decorateRenderContainers(grid);
14649
14650 // focusser only created for body
14651 if (containerId !== 'body') {
14652 return;
14653 }
14654
14655
14656
14657 if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells){
14658 $elm.attr('aria-multiselectable', true);
14659 } else {
14660 $elm.attr('aria-multiselectable', false);
14661 }
14662
14663 //add an element with no dimensions that can be used to set focus and capture keystrokes
14664 var focuser = $compile('<div class="ui-grid-focuser" role="region" aria-live="assertive" aria-atomic="false" tabindex="0" aria-controls="' + grid.id +'-aria-speakable '+ grid.id + '-grid-container' +'" aria-owns="' + grid.id + '-grid-container' + '"></div>')($scope);
14665 $elm.append(focuser);
14666
14667 focuser.on('focus', function (evt) {
14668 evt.uiGridTargetRenderContainerId = containerId;
14669 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14670 if (rowCol === null) {
14671 rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(uiGridCellNavConstants.direction.DOWN, null, null);
14672 if (rowCol.row && rowCol.col) {
14673 uiGridCtrl.cellNav.broadcastCellNav(rowCol);
14674 }
14675 }
14676 });
14677
14678 uiGridCellnavCtrl.setAriaActivedescendant = function(id){
14679 $elm.attr('aria-activedescendant', id);
14680 };
14681
14682 uiGridCellnavCtrl.removeAriaActivedescendant = function(id){
14683 if ($elm.attr('aria-activedescendant') === id){
14684 $elm.attr('aria-activedescendant', '');
14685 }
14686 };
14687
14688
14689 uiGridCtrl.focus = function () {
14690 gridUtil.focus.byElement(focuser[0]);
14691 //allow for first time grid focus
14692 };
14693
14694 var viewPortKeyDownWasRaisedForRowCol = null;
14695 // Bind to keydown events in the render container
14696 focuser.on('keydown', function (evt) {
14697 evt.uiGridTargetRenderContainerId = containerId;
14698 var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14699 var result = uiGridCtrl.cellNav.handleKeyDown(evt);
14700 if (result === null) {
14701 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyDown(evt, rowCol);
14702 viewPortKeyDownWasRaisedForRowCol = rowCol;
14703 }
14704 });
14705 //Bind to keypress events in the render container
14706 //keypress events are needed by edit function so the key press
14707 //that initiated an edit is not lost
14708 //must fire the event in a timeout so the editor can
14709 //initialize and subscribe to the event on another event loop
14710 focuser.on('keypress', function (evt) {
14711 if (viewPortKeyDownWasRaisedForRowCol) {
14712 $timeout(function () {
14713 uiGridCtrl.grid.api.cellNav.raise.viewPortKeyPress(evt, viewPortKeyDownWasRaisedForRowCol);
14714 },4);
14715
14716 viewPortKeyDownWasRaisedForRowCol = null;
14717 }
14718 });
14719
14720 $scope.$on('$destroy', function(){
14721 //Remove all event handlers associated with this focuser.
14722 focuser.off();
14723 });
14724
14725 }
14726 };
14727 }
14728 };
14729 }]);
14730
14731 module.directive('uiGridViewport', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants','$log','$compile',
14732 function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants, $log, $compile) {
14733 return {
14734 replace: true,
14735 priority: -99999, //this needs to run very last
14736 require: ['^uiGrid', '^uiGridRenderContainer', '?^uiGridCellnav'],
14737 scope: false,
14738 compile: function () {
14739 return {
14740 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
14741 },
14742 post: function ($scope, $elm, $attrs, controllers) {
14743 var uiGridCtrl = controllers[0],
14744 renderContainerCtrl = controllers[1];
14745
14746 // Skip attaching cell-nav specific logic if the directive is not attached above us
14747 if (!uiGridCtrl.grid.api.cellNav) { return; }
14748
14749 var containerId = renderContainerCtrl.containerId;
14750 //no need to process for other containers
14751 if (containerId !== 'body') {
14752 return;
14753 }
14754
14755 var grid = uiGridCtrl.grid;
14756
14757 grid.api.core.on.scrollBegin($scope, function (args) {
14758
14759 // Skip if there's no currently-focused cell
14760 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14761 if (lastRowCol === null) {
14762 return;
14763 }
14764
14765 //if not in my container, move on
14766 //todo: worry about horiz scroll
14767 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
14768 return;
14769 }
14770
14771 uiGridCtrl.cellNav.clearFocus();
14772
14773 });
14774
14775 grid.api.core.on.scrollEnd($scope, function (args) {
14776 // Skip if there's no currently-focused cell
14777 var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell();
14778 if (lastRowCol === null) {
14779 return;
14780 }
14781
14782 //if not in my container, move on
14783 //todo: worry about horiz scroll
14784 if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) {
14785 return;
14786 }
14787
14788 uiGridCtrl.cellNav.broadcastCellNav(lastRowCol);
14789
14790 });
14791
14792 grid.api.cellNav.on.navigate($scope, function () {
14793 //focus again because it can be lost
14794 uiGridCtrl.focus();
14795 });
14796
14797 }
14798 };
14799 }
14800 };
14801 }]);
14802
14803 /**
14804 * @ngdoc directive
14805 * @name ui.grid.cellNav.directive:uiGridCell
14806 * @element div
14807 * @restrict A
14808 * @description Stacks on top of ui.grid.uiGridCell to provide cell navigation
14809 */
14810 module.directive('uiGridCell', ['$timeout', '$document', 'uiGridCellNavService', 'gridUtil', 'uiGridCellNavConstants', 'uiGridConstants', 'GridRowColumn',
14811 function ($timeout, $document, uiGridCellNavService, gridUtil, uiGridCellNavConstants, uiGridConstants, GridRowColumn) {
14812 return {
14813 priority: -150, // run after default uiGridCell directive and ui.grid.edit uiGridCell
14814 restrict: 'A',
14815 require: ['^uiGrid', '?^uiGridCellnav'],
14816 scope: false,
14817 link: function ($scope, $elm, $attrs, controllers) {
14818 var uiGridCtrl = controllers[0],
14819 uiGridCellnavCtrl = controllers[1];
14820 // Skip attaching cell-nav specific logic if the directive is not attached above us
14821 if (!uiGridCtrl.grid.api.cellNav) { return; }
14822
14823 if (!$scope.col.colDef.allowCellFocus) {
14824 return;
14825 }
14826
14827 //Convinience local variables
14828 var grid = uiGridCtrl.grid;
14829 $scope.focused = false;
14830
14831 // Make this cell focusable but only with javascript/a mouse click
14832 $elm.attr('tabindex', -1);
14833
14834 // When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused
14835 $elm.find('div').on('click', function (evt) {
14836 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), evt.ctrlKey || evt.metaKey, evt);
14837
14838 evt.stopPropagation();
14839 $scope.$apply();
14840 });
14841
14842
14843 /*
14844 * XXX Hack for screen readers.
14845 * This allows the grid to focus using only the screen reader cursor.
14846 * Since the focus event doesn't include key press information we can't use it
14847 * as our primary source of the event.
14848 */
14849 $elm.on('mousedown', preventMouseDown);
14850
14851 //turn on and off for edit events
14852 if (uiGridCtrl.grid.api.edit) {
14853 uiGridCtrl.grid.api.edit.on.beginCellEdit($scope, function () {
14854 $elm.off('mousedown', preventMouseDown);
14855 });
14856
14857 uiGridCtrl.grid.api.edit.on.afterCellEdit($scope, function () {
14858 $elm.on('mousedown', preventMouseDown);
14859 });
14860
14861 uiGridCtrl.grid.api.edit.on.cancelCellEdit($scope, function () {
14862 $elm.on('mousedown', preventMouseDown);
14863 });
14864 }
14865
14866 function preventMouseDown(evt) {
14867 //Prevents the foucus event from firing if the click event is already going to fire.
14868 //If both events fire it will cause bouncing behavior.
14869 evt.preventDefault();
14870 }
14871
14872 //You can only focus on elements with a tabindex value
14873 $elm.on('focus', function (evt) {
14874 uiGridCtrl.cellNav.broadcastCellNav(new GridRowColumn($scope.row, $scope.col), false, evt);
14875 evt.stopPropagation();
14876 $scope.$apply();
14877 });
14878
14879 // This event is fired for all cells. If the cell matches, then focus is set
14880 $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol, modifierDown) {
14881 var isFocused = grid.cellNav.focusedCells.some(function(focusedRowCol, index){
14882 return (focusedRowCol.row === $scope.row && focusedRowCol.col === $scope.col);
14883 });
14884 if (isFocused){
14885 setFocused();
14886 } else {
14887 clearFocus();
14888 }
14889 });
14890
14891 function setFocused() {
14892 if (!$scope.focused){
14893 var div = $elm.find('div');
14894 div.addClass('ui-grid-cell-focus');
14895 $elm.attr('aria-selected', true);
14896 uiGridCellnavCtrl.setAriaActivedescendant($elm.attr('id'));
14897 $scope.focused = true;
14898 }
14899 }
14900
14901 function clearFocus() {
14902 if ($scope.focused){
14903 var div = $elm.find('div');
14904 div.removeClass('ui-grid-cell-focus');
14905 $elm.attr('aria-selected', false);
14906 uiGridCellnavCtrl.removeAriaActivedescendant($elm.attr('id'));
14907 $scope.focused = false;
14908 }
14909 }
14910
14911 $scope.$on('$destroy', function () {
14912 //.off withouth paramaters removes all handlers
14913 $elm.find('div').off();
14914 $elm.off();
14915 });
14916 }
14917 };
14918 }]);
14919
14920 })();
14921
14922 (function () {
14923 'use strict';
14924
14925 /**
14926 * @ngdoc overview
14927 * @name ui.grid.edit
14928 * @description
14929 *
14930 * # ui.grid.edit
14931 *
14932 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
14933 *
14934 * This module provides cell editing capability to ui.grid. The goal was to emulate keying data in a spreadsheet via
14935 * a keyboard.
14936 * <br/>
14937 * <br/>
14938 * To really get the full spreadsheet-like data entry, the ui.grid.cellNav module should be used. This will allow the
14939 * user to key data and then tab, arrow, or enter to the cells beside or below.
14940 *
14941 * <div doc-module-components="ui.grid.edit"></div>
14942 */
14943
14944 var module = angular.module('ui.grid.edit', ['ui.grid']);
14945
14946 /**
14947 * @ngdoc object
14948 * @name ui.grid.edit.constant:uiGridEditConstants
14949 *
14950 * @description constants available in edit module
14951 */
14952 module.constant('uiGridEditConstants', {
14953 EDITABLE_CELL_TEMPLATE: /EDITABLE_CELL_TEMPLATE/g,
14954 //must be lowercase because template bulder converts to lower
14955 EDITABLE_CELL_DIRECTIVE: /editable_cell_directive/g,
14956 events: {
14957 BEGIN_CELL_EDIT: 'uiGridEventBeginCellEdit',
14958 END_CELL_EDIT: 'uiGridEventEndCellEdit',
14959 CANCEL_CELL_EDIT: 'uiGridEventCancelCellEdit'
14960 }
14961 });
14962
14963 /**
14964 * @ngdoc service
14965 * @name ui.grid.edit.service:uiGridEditService
14966 *
14967 * @description Services for editing features
14968 */
14969 module.service('uiGridEditService', ['$q', 'uiGridConstants', 'gridUtil',
14970 function ($q, uiGridConstants, gridUtil) {
14971
14972 var service = {
14973
14974 initializeGrid: function (grid) {
14975
14976 service.defaultGridOptions(grid.options);
14977
14978 grid.registerColumnBuilder(service.editColumnBuilder);
14979 grid.edit = {};
14980
14981 /**
14982 * @ngdoc object
14983 * @name ui.grid.edit.api:PublicApi
14984 *
14985 * @description Public Api for edit feature
14986 */
14987 var publicApi = {
14988 events: {
14989 edit: {
14990 /**
14991 * @ngdoc event
14992 * @name afterCellEdit
14993 * @eventOf ui.grid.edit.api:PublicApi
14994 * @description raised when cell editing is complete
14995 * <pre>
14996 * gridApi.edit.on.afterCellEdit(scope,function(rowEntity, colDef){})
14997 * </pre>
14998 * @param {object} rowEntity the options.data element that was edited
14999 * @param {object} colDef the column that was edited
15000 * @param {object} newValue new value
15001 * @param {object} oldValue old value
15002 */
15003 afterCellEdit: function (rowEntity, colDef, newValue, oldValue) {
15004 },
15005 /**
15006 * @ngdoc event
15007 * @name beginCellEdit
15008 * @eventOf ui.grid.edit.api:PublicApi
15009 * @description raised when cell editing starts on a cell
15010 * <pre>
15011 * gridApi.edit.on.beginCellEdit(scope,function(rowEntity, colDef){})
15012 * </pre>
15013 * @param {object} rowEntity the options.data element that was edited
15014 * @param {object} colDef the column that was edited
15015 * @param {object} triggerEvent the event that triggered the edit. Useful to prevent losing keystrokes on some
15016 * complex editors
15017 */
15018 beginCellEdit: function (rowEntity, colDef, triggerEvent) {
15019 },
15020 /**
15021 * @ngdoc event
15022 * @name cancelCellEdit
15023 * @eventOf ui.grid.edit.api:PublicApi
15024 * @description raised when cell editing is cancelled on a cell
15025 * <pre>
15026 * gridApi.edit.on.cancelCellEdit(scope,function(rowEntity, colDef){})
15027 * </pre>
15028 * @param {object} rowEntity the options.data element that was edited
15029 * @param {object} colDef the column that was edited
15030 */
15031 cancelCellEdit: function (rowEntity, colDef) {
15032 }
15033 }
15034 },
15035 methods: {
15036 edit: { }
15037 }
15038 };
15039
15040 grid.api.registerEventsFromObject(publicApi.events);
15041 //grid.api.registerMethodsFromObject(publicApi.methods);
15042
15043 },
15044
15045 defaultGridOptions: function (gridOptions) {
15046
15047 /**
15048 * @ngdoc object
15049 * @name ui.grid.edit.api:GridOptions
15050 *
15051 * @description Options for configuring the edit feature, these are available to be
15052 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
15053 */
15054
15055 /**
15056 * @ngdoc object
15057 * @name enableCellEdit
15058 * @propertyOf ui.grid.edit.api:GridOptions
15059 * @description If defined, sets the default value for the editable flag on each individual colDefs
15060 * if their individual enableCellEdit configuration is not defined. Defaults to undefined.
15061 */
15062
15063 /**
15064 * @ngdoc object
15065 * @name cellEditableCondition
15066 * @propertyOf ui.grid.edit.api:GridOptions
15067 * @description If specified, either a value or function to be used by all columns before editing.
15068 * If falsy, then editing of cell is not allowed.
15069 * @example
15070 * <pre>
15071 * function($scope){
15072 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15073 * return true;
15074 * }
15075 * </pre>
15076 */
15077 gridOptions.cellEditableCondition = gridOptions.cellEditableCondition === undefined ? true : gridOptions.cellEditableCondition;
15078
15079 /**
15080 * @ngdoc object
15081 * @name editableCellTemplate
15082 * @propertyOf ui.grid.edit.api:GridOptions
15083 * @description If specified, cellTemplate to use as the editor for all columns.
15084 * <br/> defaults to 'ui-grid/cellTextEditor'
15085 */
15086
15087 /**
15088 * @ngdoc object
15089 * @name enableCellEditOnFocus
15090 * @propertyOf ui.grid.edit.api:GridOptions
15091 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15092 * <br/>_requires cellNav feature and the edit feature to be enabled_
15093 */
15094 //enableCellEditOnFocus can only be used if cellnav module is used
15095 gridOptions.enableCellEditOnFocus = gridOptions.enableCellEditOnFocus === undefined ? false : gridOptions.enableCellEditOnFocus;
15096 },
15097
15098 /**
15099 * @ngdoc service
15100 * @name editColumnBuilder
15101 * @methodOf ui.grid.edit.service:uiGridEditService
15102 * @description columnBuilder function that adds edit properties to grid column
15103 * @returns {promise} promise that will load any needed templates when resolved
15104 */
15105 editColumnBuilder: function (colDef, col, gridOptions) {
15106
15107 var promises = [];
15108
15109 /**
15110 * @ngdoc object
15111 * @name ui.grid.edit.api:ColumnDef
15112 *
15113 * @description Column Definition for edit feature, these are available to be
15114 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
15115 */
15116
15117 /**
15118 * @ngdoc object
15119 * @name enableCellEdit
15120 * @propertyOf ui.grid.edit.api:ColumnDef
15121 * @description enable editing on column
15122 */
15123 colDef.enableCellEdit = colDef.enableCellEdit === undefined ? (gridOptions.enableCellEdit === undefined ?
15124 (colDef.type !== 'object') : gridOptions.enableCellEdit) : colDef.enableCellEdit;
15125
15126 /**
15127 * @ngdoc object
15128 * @name cellEditableCondition
15129 * @propertyOf ui.grid.edit.api:ColumnDef
15130 * @description If specified, either a value or function evaluated before editing cell. If falsy, then editing of cell is not allowed.
15131 * @example
15132 * <pre>
15133 * function($scope){
15134 * //use $scope.row.entity and $scope.col.colDef to determine if editing is allowed
15135 * return true;
15136 * }
15137 * </pre>
15138 */
15139 colDef.cellEditableCondition = colDef.cellEditableCondition === undefined ? gridOptions.cellEditableCondition : colDef.cellEditableCondition;
15140
15141 /**
15142 * @ngdoc object
15143 * @name editableCellTemplate
15144 * @propertyOf ui.grid.edit.api:ColumnDef
15145 * @description cell template to be used when editing this column. Can be Url or text template
15146 * <br/>Defaults to gridOptions.editableCellTemplate
15147 */
15148 if (colDef.enableCellEdit) {
15149 colDef.editableCellTemplate = colDef.editableCellTemplate || gridOptions.editableCellTemplate || 'ui-grid/cellEditor';
15150
15151 promises.push(gridUtil.getTemplate(colDef.editableCellTemplate)
15152 .then(
15153 function (template) {
15154 col.editableCellTemplate = template;
15155 },
15156 function (res) {
15157 // Todo handle response error here?
15158 throw new Error("Couldn't fetch/use colDef.editableCellTemplate '" + colDef.editableCellTemplate + "'");
15159 }));
15160 }
15161
15162 /**
15163 * @ngdoc object
15164 * @name enableCellEditOnFocus
15165 * @propertyOf ui.grid.edit.api:ColumnDef
15166 * @requires ui.grid.cellNav
15167 * @description If true, then editor is invoked as soon as cell receives focus. Default false.
15168 * <br>_requires both the cellNav feature and the edit feature to be enabled_
15169 */
15170 //enableCellEditOnFocus can only be used if cellnav module is used
15171 colDef.enableCellEditOnFocus = colDef.enableCellEditOnFocus === undefined ? gridOptions.enableCellEditOnFocus : colDef.enableCellEditOnFocus;
15172
15173
15174 /**
15175 * @ngdoc string
15176 * @name editModelField
15177 * @propertyOf ui.grid.edit.api:ColumnDef
15178 * @description a bindable string value that is used when binding to edit controls instead of colDef.field
15179 * <br/> example: You have a complex property on and object like state:{abbrev:'MS',name:'Mississippi'}. The
15180 * grid should display state.name in the cell and sort/filter based on the state.name property but the editor
15181 * requires the full state object.
15182 * <br/>colDef.field = 'state.name'
15183 * <br/>colDef.editModelField = 'state'
15184 */
15185 //colDef.editModelField
15186
15187 return $q.all(promises);
15188 },
15189
15190 /**
15191 * @ngdoc service
15192 * @name isStartEditKey
15193 * @methodOf ui.grid.edit.service:uiGridEditService
15194 * @description Determines if a keypress should start editing. Decorate this service to override with your
15195 * own key events. See service decorator in angular docs.
15196 * @param {Event} evt keydown event
15197 * @returns {boolean} true if an edit should start
15198 */
15199 isStartEditKey: function (evt) {
15200 if (evt.metaKey ||
15201 evt.keyCode === uiGridConstants.keymap.ESC ||
15202 evt.keyCode === uiGridConstants.keymap.SHIFT ||
15203 evt.keyCode === uiGridConstants.keymap.CTRL ||
15204 evt.keyCode === uiGridConstants.keymap.ALT ||
15205 evt.keyCode === uiGridConstants.keymap.WIN ||
15206 evt.keyCode === uiGridConstants.keymap.CAPSLOCK ||
15207
15208 evt.keyCode === uiGridConstants.keymap.LEFT ||
15209 (evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey) ||
15210
15211 evt.keyCode === uiGridConstants.keymap.RIGHT ||
15212 evt.keyCode === uiGridConstants.keymap.TAB ||
15213
15214 evt.keyCode === uiGridConstants.keymap.UP ||
15215 (evt.keyCode === uiGridConstants.keymap.ENTER && evt.shiftKey) ||
15216
15217 evt.keyCode === uiGridConstants.keymap.DOWN ||
15218 evt.keyCode === uiGridConstants.keymap.ENTER) {
15219 return false;
15220
15221 }
15222 return true;
15223 }
15224
15225
15226 };
15227
15228 return service;
15229
15230 }]);
15231
15232 /**
15233 * @ngdoc directive
15234 * @name ui.grid.edit.directive:uiGridEdit
15235 * @element div
15236 * @restrict A
15237 *
15238 * @description Adds editing features to the ui-grid directive.
15239 *
15240 * @example
15241 <example module="app">
15242 <file name="app.js">
15243 var app = angular.module('app', ['ui.grid', 'ui.grid.edit']);
15244
15245 app.controller('MainCtrl', ['$scope', function ($scope) {
15246 $scope.data = [
15247 { name: 'Bob', title: 'CEO' },
15248 { name: 'Frank', title: 'Lowly Developer' }
15249 ];
15250
15251 $scope.columnDefs = [
15252 {name: 'name', enableCellEdit: true},
15253 {name: 'title', enableCellEdit: true}
15254 ];
15255 }]);
15256 </file>
15257 <file name="index.html">
15258 <div ng-controller="MainCtrl">
15259 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-edit></div>
15260 </div>
15261 </file>
15262 </example>
15263 */
15264 module.directive('uiGridEdit', ['gridUtil', 'uiGridEditService', function (gridUtil, uiGridEditService) {
15265 return {
15266 replace: true,
15267 priority: 0,
15268 require: '^uiGrid',
15269 scope: false,
15270 compile: function () {
15271 return {
15272 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
15273 uiGridEditService.initializeGrid(uiGridCtrl.grid);
15274 },
15275 post: function ($scope, $elm, $attrs, uiGridCtrl) {
15276 }
15277 };
15278 }
15279 };
15280 }]);
15281
15282 /**
15283 * @ngdoc directive
15284 * @name ui.grid.edit.directive:uiGridRenderContainer
15285 * @element div
15286 * @restrict A
15287 *
15288 * @description Adds keydown listeners to renderContainer element so we can capture when to begin edits
15289 *
15290 */
15291 module.directive('uiGridViewport', [ 'uiGridEditConstants',
15292 function ( uiGridEditConstants) {
15293 return {
15294 replace: true,
15295 priority: -99998, //run before cellNav
15296 require: ['^uiGrid', '^uiGridRenderContainer'],
15297 scope: false,
15298 compile: function () {
15299 return {
15300 post: function ($scope, $elm, $attrs, controllers) {
15301 var uiGridCtrl = controllers[0];
15302
15303 // Skip attaching if edit and cellNav is not enabled
15304 if (!uiGridCtrl.grid.api.edit || !uiGridCtrl.grid.api.cellNav) { return; }
15305
15306 var containerId = controllers[1].containerId;
15307 //no need to process for other containers
15308 if (containerId !== 'body') {
15309 return;
15310 }
15311
15312 //refocus on the grid
15313 $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
15314 uiGridCtrl.focus();
15315 });
15316 $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
15317 uiGridCtrl.focus();
15318 });
15319
15320 }
15321 };
15322 }
15323 };
15324 }]);
15325
15326 /**
15327 * @ngdoc directive
15328 * @name ui.grid.edit.directive:uiGridCell
15329 * @element div
15330 * @restrict A
15331 *
15332 * @description Stacks on top of ui.grid.uiGridCell to provide in-line editing capabilities to the cell
15333 * Editing Actions.
15334 *
15335 * Binds edit start events to the uiGridCell element. When the events fire, the gridCell element is appended
15336 * with the columnDef.editableCellTemplate element ('cellEditor.html' by default).
15337 *
15338 * The editableCellTemplate should respond to uiGridEditConstants.events.BEGIN\_CELL\_EDIT angular event
15339 * and do the initial steps needed to edit the cell (setfocus on input element, etc).
15340 *
15341 * When the editableCellTemplate recognizes that the editing is ended (blur event, Enter key, etc.)
15342 * it should emit the uiGridEditConstants.events.END\_CELL\_EDIT event.
15343 *
15344 * If editableCellTemplate recognizes that the editing has been cancelled (esc key)
15345 * it should emit the uiGridEditConstants.events.CANCEL\_CELL\_EDIT event. The original value
15346 * will be set back on the model by the uiGridCell directive.
15347 *
15348 * Events that invoke editing:
15349 * - dblclick
15350 * - F2 keydown (when using cell selection)
15351 *
15352 * Events that end editing:
15353 * - Dependent on the specific editableCellTemplate
15354 * - Standards should be blur and enter keydown
15355 *
15356 * Events that cancel editing:
15357 * - Dependent on the specific editableCellTemplate
15358 * - Standards should be Esc keydown
15359 *
15360 * Grid Events that end editing:
15361 * - uiGridConstants.events.GRID_SCROLL
15362 *
15363 */
15364
15365 /**
15366 * @ngdoc object
15367 * @name ui.grid.edit.api:GridRow
15368 *
15369 * @description GridRow options for edit feature, these are available to be
15370 * set internally only, by other features
15371 */
15372
15373 /**
15374 * @ngdoc object
15375 * @name enableCellEdit
15376 * @propertyOf ui.grid.edit.api:GridRow
15377 * @description enable editing on row, grouping for example might disable editing on group header rows
15378 */
15379
15380 module.directive('uiGridCell',
15381 ['$compile', '$injector', '$timeout', 'uiGridConstants', 'uiGridEditConstants', 'gridUtil', '$parse', 'uiGridEditService', '$rootScope',
15382 function ($compile, $injector, $timeout, uiGridConstants, uiGridEditConstants, gridUtil, $parse, uiGridEditService, $rootScope) {
15383 var touchstartTimeout = 500;
15384 if ($injector.has('uiGridCellNavService')) {
15385 var uiGridCellNavService = $injector.get('uiGridCellNavService');
15386 }
15387
15388 return {
15389 priority: -100, // run after default uiGridCell directive
15390 restrict: 'A',
15391 scope: false,
15392 require: '?^uiGrid',
15393 link: function ($scope, $elm, $attrs, uiGridCtrl) {
15394 var html;
15395 var origCellValue;
15396 var inEdit = false;
15397 var cellModel;
15398 var cancelTouchstartTimeout;
15399
15400 var editCellScope;
15401
15402 if (!$scope.col.colDef.enableCellEdit) {
15403 return;
15404 }
15405
15406 var cellNavNavigateDereg = function() {};
15407 var viewPortKeyDownDereg = function() {};
15408
15409
15410 var setEditable = function() {
15411 if ($scope.col.colDef.enableCellEdit && $scope.row.enableCellEdit !== false) {
15412 if (!$scope.beginEditEventsWired) { //prevent multiple attachments
15413 registerBeginEditEvents();
15414 }
15415 } else {
15416 if ($scope.beginEditEventsWired) {
15417 cancelBeginEditEvents();
15418 }
15419 }
15420 };
15421
15422 setEditable();
15423
15424 var rowWatchDereg = $scope.$watch('row', function (n, o) {
15425 if (n !== o) {
15426 setEditable();
15427 }
15428 });
15429
15430
15431 $scope.$on( '$destroy', rowWatchDereg );
15432
15433 function registerBeginEditEvents() {
15434 $elm.on('dblclick', beginEdit);
15435
15436 // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit
15437 $elm.on('touchstart', touchStart);
15438
15439 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15440
15441 viewPortKeyDownDereg = uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
15442 if (rowCol === null) {
15443 return;
15444 }
15445
15446 if (rowCol.row === $scope.row && rowCol.col === $scope.col && !$scope.col.colDef.enableCellEditOnFocus) {
15447 //important to do this before scrollToIfNecessary
15448 beginEditKeyDown(evt);
15449 }
15450 });
15451
15452 cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) {
15453 if ($scope.col.colDef.enableCellEditOnFocus) {
15454 // Don't begin edit if the cell hasn't changed
15455 if ((!oldRowCol || newRowCol.row !== oldRowCol.row || newRowCol.col !== oldRowCol.col) &&
15456 newRowCol.row === $scope.row && newRowCol.col === $scope.col) {
15457 $timeout(function () {
15458 beginEdit();
15459 });
15460 }
15461 }
15462 });
15463 }
15464
15465 $scope.beginEditEventsWired = true;
15466
15467 }
15468
15469 function touchStart(event) {
15470 // jQuery masks events
15471 if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) {
15472 event = event.originalEvent;
15473 }
15474
15475 // Bind touchend handler
15476 $elm.on('touchend', touchEnd);
15477
15478 // Start a timeout
15479 cancelTouchstartTimeout = $timeout(function() { }, touchstartTimeout);
15480
15481 // Timeout's done! Start the edit
15482 cancelTouchstartTimeout.then(function () {
15483 // Use setTimeout to start the edit because beginEdit expects to be outside of $digest
15484 setTimeout(beginEdit, 0);
15485
15486 // Undbind the touchend handler, we don't need it anymore
15487 $elm.off('touchend', touchEnd);
15488 });
15489 }
15490
15491 // Cancel any touchstart timeout
15492 function touchEnd(event) {
15493 $timeout.cancel(cancelTouchstartTimeout);
15494 $elm.off('touchend', touchEnd);
15495 }
15496
15497 function cancelBeginEditEvents() {
15498 $elm.off('dblclick', beginEdit);
15499 $elm.off('keydown', beginEditKeyDown);
15500 $elm.off('touchstart', touchStart);
15501 cellNavNavigateDereg();
15502 viewPortKeyDownDereg();
15503 $scope.beginEditEventsWired = false;
15504 }
15505
15506 function beginEditKeyDown(evt) {
15507 if (uiGridEditService.isStartEditKey(evt)) {
15508 beginEdit(evt);
15509 }
15510 }
15511
15512 function shouldEdit(col, row) {
15513 return !row.isSaving &&
15514 ( angular.isFunction(col.colDef.cellEditableCondition) ?
15515 col.colDef.cellEditableCondition($scope) :
15516 col.colDef.cellEditableCondition );
15517 }
15518
15519
15520 function beginEdit(triggerEvent) {
15521 //we need to scroll the cell into focus before invoking the editor
15522 $scope.grid.api.core.scrollToIfNecessary($scope.row, $scope.col)
15523 .then(function () {
15524 beginEditAfterScroll(triggerEvent);
15525 });
15526 }
15527
15528 /**
15529 * @ngdoc property
15530 * @name editDropdownOptionsArray
15531 * @propertyOf ui.grid.edit.api:ColumnDef
15532 * @description an array of values in the format
15533 * [ {id: xxx, value: xxx} ], which is populated
15534 * into the edit dropdown
15535 *
15536 */
15537 /**
15538 * @ngdoc property
15539 * @name editDropdownIdLabel
15540 * @propertyOf ui.grid.edit.api:ColumnDef
15541 * @description the label for the "id" field
15542 * in the editDropdownOptionsArray. Defaults
15543 * to 'id'
15544 * @example
15545 * <pre>
15546 * $scope.gridOptions = {
15547 * columnDefs: [
15548 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15549 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
15550 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
15551 * ],
15552 * </pre>
15553 *
15554 */
15555 /**
15556 * @ngdoc property
15557 * @name editDropdownRowEntityOptionsArrayPath
15558 * @propertyOf ui.grid.edit.api:ColumnDef
15559 * @description a path to a property on row.entity containing an
15560 * array of values in the format
15561 * [ {id: xxx, value: xxx} ], which will be used to populate
15562 * the edit dropdown. This can be used when the dropdown values are dependent on
15563 * the backing row entity.
15564 * If this property is set then editDropdownOptionsArray will be ignored.
15565 * @example
15566 * <pre>
15567 * $scope.gridOptions = {
15568 * columnDefs: [
15569 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15570 * editDropdownRowEntityOptionsArrayPath: 'foo.bars[0].baz',
15571 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
15572 * ],
15573 * </pre>
15574 *
15575 */
15576 /**
15577 * @ngdoc property
15578 * @name editDropdownValueLabel
15579 * @propertyOf ui.grid.edit.api:ColumnDef
15580 * @description the label for the "value" field
15581 * in the editDropdownOptionsArray. Defaults
15582 * to 'value'
15583 * @example
15584 * <pre>
15585 * $scope.gridOptions = {
15586 * columnDefs: [
15587 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15588 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
15589 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status' }
15590 * ],
15591 * </pre>
15592 *
15593 */
15594 /**
15595 * @ngdoc property
15596 * @name editDropdownFilter
15597 * @propertyOf ui.grid.edit.api:ColumnDef
15598 * @description A filter that you would like to apply to the values in the options list
15599 * of the dropdown. For example if you were using angular-translate you might set this
15600 * to `'translate'`
15601 * @example
15602 * <pre>
15603 * $scope.gridOptions = {
15604 * columnDefs: [
15605 * {name: 'status', editableCellTemplate: 'ui-grid/dropdownEditor',
15606 * editDropdownOptionsArray: [{code: 1, status: 'active'}, {code: 2, status: 'inactive'}],
15607 * editDropdownIdLabel: 'code', editDropdownValueLabel: 'status', editDropdownFilter: 'translate' }
15608 * ],
15609 * </pre>
15610 *
15611 */
15612 function beginEditAfterScroll(triggerEvent) {
15613 // If we are already editing, then just skip this so we don't try editing twice...
15614 if (inEdit) {
15615 return;
15616 }
15617
15618 if (!shouldEdit($scope.col, $scope.row)) {
15619 return;
15620 }
15621
15622
15623 cellModel = $parse($scope.row.getQualifiedColField($scope.col));
15624 //get original value from the cell
15625 origCellValue = cellModel($scope);
15626
15627 html = $scope.col.editableCellTemplate;
15628
15629 if ($scope.col.colDef.editModelField) {
15630 html = html.replace(uiGridConstants.MODEL_COL_FIELD, gridUtil.preEval('row.entity.' + $scope.col.colDef.editModelField));
15631 }
15632 else {
15633 html = html.replace(uiGridConstants.MODEL_COL_FIELD, $scope.row.getQualifiedColField($scope.col));
15634 }
15635
15636 html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)');
15637
15638 var optionFilter = $scope.col.colDef.editDropdownFilter ? '|' + $scope.col.colDef.editDropdownFilter : '';
15639 html = html.replace(uiGridConstants.CUSTOM_FILTERS, optionFilter);
15640
15641 var inputType = 'text';
15642 switch ($scope.col.colDef.type){
15643 case 'boolean':
15644 inputType = 'checkbox';
15645 break;
15646 case 'number':
15647 inputType = 'number';
15648 break;
15649 case 'date':
15650 inputType = 'date';
15651 break;
15652 }
15653 html = html.replace('INPUT_TYPE', inputType);
15654
15655 var editDropdownRowEntityOptionsArrayPath = $scope.col.colDef.editDropdownRowEntityOptionsArrayPath;
15656 if (editDropdownRowEntityOptionsArrayPath) {
15657 $scope.editDropdownOptionsArray = resolveObjectFromPath($scope.row.entity, editDropdownRowEntityOptionsArrayPath);
15658 }
15659 else {
15660 $scope.editDropdownOptionsArray = $scope.col.colDef.editDropdownOptionsArray;
15661 }
15662 $scope.editDropdownIdLabel = $scope.col.colDef.editDropdownIdLabel ? $scope.col.colDef.editDropdownIdLabel : 'id';
15663 $scope.editDropdownValueLabel = $scope.col.colDef.editDropdownValueLabel ? $scope.col.colDef.editDropdownValueLabel : 'value';
15664
15665 var cellElement;
15666 var createEditor = function(){
15667 inEdit = true;
15668 cancelBeginEditEvents();
15669 var cellElement = angular.element(html);
15670 $elm.append(cellElement);
15671 editCellScope = $scope.$new();
15672 $compile(cellElement)(editCellScope);
15673 var gridCellContentsEl = angular.element($elm.children()[0]);
15674 gridCellContentsEl.addClass('ui-grid-cell-contents-hidden');
15675 };
15676 if (!$rootScope.$$phase) {
15677 $scope.$apply(createEditor);
15678 } else {
15679 createEditor();
15680 }
15681
15682 //stop editing when grid is scrolled
15683 var deregOnGridScroll = $scope.col.grid.api.core.on.scrollBegin($scope, function () {
15684 if ($scope.grid.disableScrolling) {
15685 return;
15686 }
15687 endEdit();
15688 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
15689 deregOnGridScroll();
15690 deregOnEndCellEdit();
15691 deregOnCancelCellEdit();
15692 });
15693
15694 //end editing
15695 var deregOnEndCellEdit = $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () {
15696 endEdit();
15697 $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue);
15698 deregOnEndCellEdit();
15699 deregOnGridScroll();
15700 deregOnCancelCellEdit();
15701 });
15702
15703 //cancel editing
15704 var deregOnCancelCellEdit = $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () {
15705 cancelEdit();
15706 deregOnCancelCellEdit();
15707 deregOnGridScroll();
15708 deregOnEndCellEdit();
15709 });
15710
15711 $scope.$broadcast(uiGridEditConstants.events.BEGIN_CELL_EDIT, triggerEvent);
15712 $timeout(function () {
15713 //execute in a timeout to give any complex editor templates a cycle to completely render
15714 $scope.grid.api.edit.raise.beginCellEdit($scope.row.entity, $scope.col.colDef, triggerEvent);
15715 });
15716 }
15717
15718 function endEdit() {
15719 $scope.grid.disableScrolling = false;
15720 if (!inEdit) {
15721 return;
15722 }
15723
15724 //sometimes the events can't keep up with the keyboard and grid focus is lost, so always focus
15725 //back to grid here. The focus call needs to be before the $destroy and removal of the control,
15726 //otherwise ng-model-options of UpdateOn: 'blur' will not work.
15727 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15728 uiGridCtrl.focus();
15729 }
15730
15731 var gridCellContentsEl = angular.element($elm.children()[0]);
15732 //remove edit element
15733 editCellScope.$destroy();
15734 angular.element($elm.children()[1]).remove();
15735 gridCellContentsEl.removeClass('ui-grid-cell-contents-hidden');
15736 inEdit = false;
15737 registerBeginEditEvents();
15738 $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.EDIT );
15739 }
15740
15741 function cancelEdit() {
15742 $scope.grid.disableScrolling = false;
15743 if (!inEdit) {
15744 return;
15745 }
15746 cellModel.assign($scope, origCellValue);
15747 $scope.$apply();
15748
15749 $scope.grid.api.edit.raise.cancelCellEdit($scope.row.entity, $scope.col.colDef);
15750 endEdit();
15751 }
15752
15753 // resolves a string path against the given object
15754 // shamelessly borrowed from
15755 // http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
15756 function resolveObjectFromPath(object, path) {
15757 path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
15758 path = path.replace(/^\./, ''); // strip a leading dot
15759 var a = path.split('.');
15760 while (a.length) {
15761 var n = a.shift();
15762 if (n in object) {
15763 object = object[n];
15764 } else {
15765 return;
15766 }
15767 }
15768 return object;
15769 }
15770
15771 }
15772 };
15773 }]);
15774
15775 /**
15776 * @ngdoc directive
15777 * @name ui.grid.edit.directive:uiGridEditor
15778 * @element div
15779 * @restrict A
15780 *
15781 * @description input editor directive for editable fields.
15782 * Provides EndEdit and CancelEdit events
15783 *
15784 * Events that end editing:
15785 * blur and enter keydown
15786 *
15787 * Events that cancel editing:
15788 * - Esc keydown
15789 *
15790 */
15791 module.directive('uiGridEditor',
15792 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', 'uiGridEditService',
15793 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout, uiGridEditService) {
15794 return {
15795 scope: true,
15796 require: ['?^uiGrid', '?^uiGridRenderContainer', 'ngModel'],
15797 compile: function () {
15798 return {
15799 pre: function ($scope, $elm, $attrs) {
15800
15801 },
15802 post: function ($scope, $elm, $attrs, controllers) {
15803 var uiGridCtrl, renderContainerCtrl, ngModel;
15804 if (controllers[0]) { uiGridCtrl = controllers[0]; }
15805 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
15806 if (controllers[2]) { ngModel = controllers[2]; }
15807
15808 //set focus at start of edit
15809 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function (evt,triggerEvent) {
15810 $timeout(function () {
15811 $elm[0].focus();
15812 //only select text if it is not being replaced below in the cellNav viewPortKeyPress
15813 if ($scope.col.colDef.enableCellEditOnFocus || !(uiGridCtrl && uiGridCtrl.grid.api.cellNav)) {
15814 $elm[0].select();
15815 }
15816 else {
15817 //some browsers (Chrome) stupidly, imo, support the w3 standard that number, email, ...
15818 //fields should not allow setSelectionRange. We ignore the error for those browsers
15819 //https://www.w3.org/Bugs/Public/show_bug.cgi?id=24796
15820 try {
15821 $elm[0].setSelectionRange($elm[0].value.length, $elm[0].value.length);
15822 }
15823 catch (ex) {
15824 //ignore
15825 }
15826 }
15827 });
15828
15829 //set the keystroke that started the edit event
15830 //we must do this because the BeginEdit is done in a different event loop than the intitial
15831 //keydown event
15832 //fire this event for the keypress that is received
15833 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15834 var viewPortKeyDownUnregister = uiGridCtrl.grid.api.cellNav.on.viewPortKeyPress($scope, function (evt, rowCol) {
15835 if (uiGridEditService.isStartEditKey(evt)) {
15836 ngModel.$setViewValue(String.fromCharCode(evt.keyCode), evt);
15837 ngModel.$render();
15838 }
15839 viewPortKeyDownUnregister();
15840 });
15841 }
15842
15843 $elm.on('blur', function (evt) {
15844 $scope.stopEdit(evt);
15845 });
15846 });
15847
15848
15849 $scope.deepEdit = false;
15850
15851 $scope.stopEdit = function (evt) {
15852 if ($scope.inputForm && !$scope.inputForm.$valid) {
15853 evt.stopPropagation();
15854 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
15855 }
15856 else {
15857 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
15858 }
15859 $scope.deepEdit = false;
15860 };
15861
15862
15863 $elm.on('click', function (evt) {
15864 if ($elm[0].type !== 'checkbox') {
15865 $scope.deepEdit = true;
15866 $timeout(function () {
15867 $scope.grid.disableScrolling = true;
15868 });
15869 }
15870 });
15871
15872 $elm.on('keydown', function (evt) {
15873 switch (evt.keyCode) {
15874 case uiGridConstants.keymap.ESC:
15875 evt.stopPropagation();
15876 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
15877 break;
15878 }
15879
15880 if ($scope.deepEdit &&
15881 (evt.keyCode === uiGridConstants.keymap.LEFT ||
15882 evt.keyCode === uiGridConstants.keymap.RIGHT ||
15883 evt.keyCode === uiGridConstants.keymap.UP ||
15884 evt.keyCode === uiGridConstants.keymap.DOWN)) {
15885 evt.stopPropagation();
15886 }
15887 // Pass the keydown event off to the cellNav service, if it exists
15888 else if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
15889 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
15890 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
15891 $scope.stopEdit(evt);
15892 }
15893 }
15894 else {
15895 //handle enter and tab for editing not using cellNav
15896 switch (evt.keyCode) {
15897 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
15898 case uiGridConstants.keymap.TAB:
15899 evt.stopPropagation();
15900 evt.preventDefault();
15901 $scope.stopEdit(evt);
15902 break;
15903 }
15904 }
15905
15906 return true;
15907 });
15908 }
15909 };
15910 }
15911 };
15912 }]);
15913
15914 /**
15915 * @ngdoc directive
15916 * @name ui.grid.edit.directive:input
15917 * @element input
15918 * @restrict E
15919 *
15920 * @description directive to provide binding between input[date] value and ng-model for angular 1.2
15921 * It is similar to input[date] directive of angular 1.3
15922 *
15923 * Supported date format for input is 'yyyy-MM-dd'
15924 * The directive will set the $valid property of input element and the enclosing form to false if
15925 * model is invalid date or value of input is entered wrong.
15926 *
15927 */
15928 module.directive('uiGridEditor', ['$filter', function ($filter) {
15929 function parseDateString(dateString) {
15930 if (typeof(dateString) === 'undefined' || dateString === '') {
15931 return null;
15932 }
15933 var parts = dateString.split('-');
15934 if (parts.length !== 3) {
15935 return null;
15936 }
15937 var year = parseInt(parts[0], 10);
15938 var month = parseInt(parts[1], 10);
15939 var day = parseInt(parts[2], 10);
15940
15941 if (month < 1 || year < 1 || day < 1) {
15942 return null;
15943 }
15944 return new Date(year, (month - 1), day);
15945 }
15946 return {
15947 priority: -100, // run after default uiGridEditor directive
15948 require: '?ngModel',
15949 link: function (scope, element, attrs, ngModel) {
15950
15951 if (angular.version.minor === 2 && attrs.type && attrs.type === 'date' && ngModel) {
15952
15953 ngModel.$formatters.push(function (modelValue) {
15954 ngModel.$setValidity(null,(!modelValue || !isNaN(modelValue.getTime())));
15955 return $filter('date')(modelValue, 'yyyy-MM-dd');
15956 });
15957
15958 ngModel.$parsers.push(function (viewValue) {
15959 if (viewValue && viewValue.length > 0) {
15960 var dateValue = parseDateString(viewValue);
15961 ngModel.$setValidity(null, (dateValue && !isNaN(dateValue.getTime())));
15962 return dateValue;
15963 }
15964 else {
15965 ngModel.$setValidity(null, true);
15966 return null;
15967 }
15968 });
15969 }
15970 }
15971 };
15972 }]);
15973
15974
15975 /**
15976 * @ngdoc directive
15977 * @name ui.grid.edit.directive:uiGridEditDropdown
15978 * @element div
15979 * @restrict A
15980 *
15981 * @description dropdown editor for editable fields.
15982 * Provides EndEdit and CancelEdit events
15983 *
15984 * Events that end editing:
15985 * blur and enter keydown, and any left/right nav
15986 *
15987 * Events that cancel editing:
15988 * - Esc keydown
15989 *
15990 */
15991 module.directive('uiGridEditDropdown',
15992 ['uiGridConstants', 'uiGridEditConstants',
15993 function (uiGridConstants, uiGridEditConstants) {
15994 return {
15995 require: ['?^uiGrid', '?^uiGridRenderContainer'],
15996 scope: true,
15997 compile: function () {
15998 return {
15999 pre: function ($scope, $elm, $attrs) {
16000
16001 },
16002 post: function ($scope, $elm, $attrs, controllers) {
16003 var uiGridCtrl = controllers[0];
16004 var renderContainerCtrl = controllers[1];
16005
16006 //set focus at start of edit
16007 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16008 $elm[0].focus();
16009 $elm[0].style.width = ($elm[0].parentElement.offsetWidth - 1) + 'px';
16010 $elm.on('blur', function (evt) {
16011 $scope.stopEdit(evt);
16012 });
16013 });
16014
16015
16016 $scope.stopEdit = function (evt) {
16017 // no need to validate a dropdown - invalid values shouldn't be
16018 // available in the list
16019 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16020 };
16021
16022 $elm.on('keydown', function (evt) {
16023 switch (evt.keyCode) {
16024 case uiGridConstants.keymap.ESC:
16025 evt.stopPropagation();
16026 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16027 break;
16028 }
16029 if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) {
16030 evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId;
16031 if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) {
16032 $scope.stopEdit(evt);
16033 }
16034 }
16035 else {
16036 //handle enter and tab for editing not using cellNav
16037 switch (evt.keyCode) {
16038 case uiGridConstants.keymap.ENTER: // Enter (Leave Field)
16039 case uiGridConstants.keymap.TAB:
16040 evt.stopPropagation();
16041 evt.preventDefault();
16042 $scope.stopEdit(evt);
16043 break;
16044 }
16045 }
16046 return true;
16047 });
16048 }
16049 };
16050 }
16051 };
16052 }]);
16053
16054 /**
16055 * @ngdoc directive
16056 * @name ui.grid.edit.directive:uiGridEditFileChooser
16057 * @element div
16058 * @restrict A
16059 *
16060 * @description input editor directive for editable fields.
16061 * Provides EndEdit and CancelEdit events
16062 *
16063 * Events that end editing:
16064 * blur and enter keydown
16065 *
16066 * Events that cancel editing:
16067 * - Esc keydown
16068 *
16069 */
16070 module.directive('uiGridEditFileChooser',
16071 ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout',
16072 function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) {
16073 return {
16074 scope: true,
16075 require: ['?^uiGrid', '?^uiGridRenderContainer'],
16076 compile: function () {
16077 return {
16078 pre: function ($scope, $elm, $attrs) {
16079
16080 },
16081 post: function ($scope, $elm, $attrs, controllers) {
16082 var uiGridCtrl, renderContainerCtrl;
16083 if (controllers[0]) { uiGridCtrl = controllers[0]; }
16084 if (controllers[1]) { renderContainerCtrl = controllers[1]; }
16085 var grid = uiGridCtrl.grid;
16086
16087 var handleFileSelect = function( event ){
16088 var target = event.srcElement || event.target;
16089
16090 if (target && target.files && target.files.length > 0) {
16091 /**
16092 * @ngdoc property
16093 * @name editFileChooserCallback
16094 * @propertyOf ui.grid.edit.api:ColumnDef
16095 * @description A function that should be called when any files have been chosen
16096 * by the user. You should use this to process the files appropriately for your
16097 * application.
16098 *
16099 * It passes the gridCol, the gridRow (from which you can get gridRow.entity),
16100 * and the files. The files are in the format as returned from the file chooser,
16101 * an array of files, with each having useful information such as:
16102 * - `files[0].lastModifiedDate`
16103 * - `files[0].name`
16104 * - `files[0].size` (appears to be in bytes)
16105 * - `files[0].type` (MIME type by the looks)
16106 *
16107 * Typically you would do something with these files - most commonly you would
16108 * use the filename or read the file itself in. The example function does both.
16109 *
16110 * @example
16111 * <pre>
16112 * editFileChooserCallBack: function(gridRow, gridCol, files ){
16113 * // ignore all but the first file, it can only choose one anyway
16114 * // set the filename into this column
16115 * gridRow.entity.filename = file[0].name;
16116 *
16117 * // read the file and set it into a hidden column, which we may do stuff with later
16118 * var setFile = function(fileContent){
16119 * gridRow.entity.file = fileContent.currentTarget.result;
16120 * };
16121 * var reader = new FileReader();
16122 * reader.onload = setFile;
16123 * reader.readAsText( files[0] );
16124 * }
16125 * </pre>
16126 */
16127 if ( typeof($scope.col.colDef.editFileChooserCallback) === 'function' ) {
16128 $scope.col.colDef.editFileChooserCallback($scope.row, $scope.col, target.files);
16129 } else {
16130 gridUtil.logError('You need to set colDef.editFileChooserCallback to use the file chooser');
16131 }
16132
16133 target.form.reset();
16134 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16135 } else {
16136 $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT);
16137 }
16138 };
16139
16140 $elm[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
16141
16142 $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () {
16143 $elm[0].focus();
16144 $elm[0].select();
16145
16146 $elm.on('blur', function (evt) {
16147 $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
16148 });
16149 });
16150 }
16151 };
16152 }
16153 };
16154 }]);
16155
16156
16157 })();
16158
16159 (function () {
16160 'use strict';
16161
16162 /**
16163 * @ngdoc overview
16164 * @name ui.grid.expandable
16165 * @description
16166 *
16167 * # ui.grid.expandable
16168 *
16169 * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
16170 *
16171 * This module provides the ability to create subgrids with the ability to expand a row
16172 * to show the subgrid.
16173 *
16174 * <div doc-module-components="ui.grid.expandable"></div>
16175 */
16176 var module = angular.module('ui.grid.expandable', ['ui.grid']);
16177
16178 /**
16179 * @ngdoc service
16180 * @name ui.grid.expandable.service:uiGridExpandableService
16181 *
16182 * @description Services for the expandable grid
16183 */
16184 module.service('uiGridExpandableService', ['gridUtil', '$compile', function (gridUtil, $compile) {
16185 var service = {
16186 initializeGrid: function (grid) {
16187
16188 grid.expandable = {};
16189 grid.expandable.expandedAll = false;
16190
16191 /**
16192 * @ngdoc object
16193 * @name enableExpandable
16194 * @propertyOf ui.grid.expandable.api:GridOptions
16195 * @description Whether or not to use expandable feature, allows you to turn off expandable on specific grids
16196 * within your application, or in specific modes on _this_ grid. Defaults to true.
16197 * @example
16198 * <pre>
16199 * $scope.gridOptions = {
16200 * enableExpandable: false
16201 * }
16202 * </pre>
16203 */
16204 grid.options.enableExpandable = grid.options.enableExpandable !== false;
16205
16206 /**
16207 * @ngdoc object
16208 * @name expandableRowHeight
16209 * @propertyOf ui.grid.expandable.api:GridOptions
16210 * @description Height in pixels of the expanded subgrid. Defaults to
16211 * 150
16212 * @example
16213 * <pre>
16214 * $scope.gridOptions = {
16215 * expandableRowHeight: 150
16216 * }
16217 * </pre>
16218 */
16219 grid.options.expandableRowHeight = grid.options.expandableRowHeight || 150;
16220
16221 /**
16222 * @ngdoc object
16223 * @name
16224 * @propertyOf ui.grid.expandable.api:GridOptions
16225 * @description Width in pixels of the expandable column. Defaults to 40
16226 * @example
16227 * <pre>
16228 * $scope.gridOptions = {
16229 * expandableRowHeaderWidth: 40
16230 * }
16231 * </pre>
16232 */
16233 grid.options.expandableRowHeaderWidth = grid.options.expandableRowHeaderWidth || 40;
16234
16235 /**
16236 * @ngdoc object
16237 * @name expandableRowTemplate
16238 * @propertyOf ui.grid.expandable.api:GridOptions
16239 * @description Mandatory. The template for your expanded row
16240 * @example
16241 * <pre>
16242 * $scope.gridOptions = {
16243 * expandableRowTemplate: 'expandableRowTemplate.html'
16244 * }
16245 * </pre>
16246 */
16247 if ( grid.options.enableExpandable && !grid.options.expandableRowTemplate ){
16248 gridUtil.logError( 'You have not set the expandableRowTemplate, disabling expandable module' );
16249 grid.options.enableExpandable = false;
16250 }
16251
16252 /**
16253 * @ngdoc object
16254 * @name ui.grid.expandable.api:PublicApi
16255 *
16256 * @description Public Api for expandable feature
16257 */
16258 /**
16259 * @ngdoc object
16260 * @name ui.grid.expandable.api:GridOptions
16261 *
16262 * @description Options for configuring the expandable feature, these are available to be
16263 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
16264 */
16265 var publicApi = {
16266 events: {
16267 expandable: {
16268 /**
16269 * @ngdoc event
16270 * @name rowExpandedStateChanged
16271 * @eventOf ui.grid.expandable.api:PublicApi
16272 * @description raised when cell editing is complete
16273 * <pre>
16274 * gridApi.expandable.on.rowExpandedStateChanged(scope,function(row){})
16275 * </pre>
16276 * @param {GridRow} row the row that was expanded
16277 */
16278 rowExpandedBeforeStateChanged: function(scope,row){
16279 },
16280 rowExpandedStateChanged: function (scope, row) {
16281 }
16282 }
16283 },
16284
16285 methods: {
16286 expandable: {
16287 /**
16288 * @ngdoc method
16289 * @name toggleRowExpansion
16290 * @methodOf ui.grid.expandable.api:PublicApi
16291 * @description Toggle a specific row
16292 * <pre>
16293 * gridApi.expandable.toggleRowExpansion(rowEntity);
16294 * </pre>
16295 * @param {object} rowEntity the data entity for the row you want to expand
16296 */
16297 toggleRowExpansion: function (rowEntity) {
16298 var row = grid.getRow(rowEntity);
16299 if (row !== null) {
16300 service.toggleRowExpansion(grid, row);
16301 }
16302 },
16303
16304 /**
16305 * @ngdoc method
16306 * @name expandAllRows
16307 * @methodOf ui.grid.expandable.api:PublicApi
16308 * @description Expand all subgrids.
16309 * <pre>
16310 * gridApi.expandable.expandAllRows();
16311 * </pre>
16312 */
16313 expandAllRows: function() {
16314 service.expandAllRows(grid);
16315 },
16316
16317 /**
16318 * @ngdoc method
16319 * @name collapseAllRows
16320 * @methodOf ui.grid.expandable.api:PublicApi
16321 * @description Collapse all subgrids.
16322 * <pre>
16323 * gridApi.expandable.collapseAllRows();
16324 * </pre>
16325 */
16326 collapseAllRows: function() {
16327 service.collapseAllRows(grid);
16328 },
16329
16330 /**
16331 * @ngdoc method
16332 * @name toggleAllRows
16333 * @methodOf ui.grid.expandable.api:PublicApi
16334 * @description Toggle all subgrids.
16335 * <pre>
16336 * gridApi.expandable.toggleAllRows();
16337 * </pre>
16338 */
16339 toggleAllRows: function() {
16340 service.toggleAllRows(grid);
16341 }
16342 }
16343 }
16344 };
16345 grid.api.registerEventsFromObject(publicApi.events);
16346 grid.api.registerMethodsFromObject(publicApi.methods);
16347 },
16348
16349 toggleRowExpansion: function (grid, row) {
16350 // trigger the "before change" event. Can change row height dynamically this way.
16351 grid.api.expandable.raise.rowExpandedBeforeStateChanged(row);
16352 row.isExpanded = !row.isExpanded;
16353 if (angular.isUndefined(row.expandedRowHeight)){
16354 row.expandedRowHeight = grid.options.expandableRowHeight;
16355 }
16356
16357 if (row.isExpanded) {
16358 row.height = row.grid.options.rowHeight + row.expandedRowHeight;
16359 }
16360 else {
16361 row.height = row.grid.options.rowHeight;
16362 grid.expandable.expandedAll = false;
16363 }
16364 grid.api.expandable.raise.rowExpandedStateChanged(row);
16365 },
16366
16367 expandAllRows: function(grid, $scope) {
16368 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16369 if (!row.isExpanded) {
16370 service.toggleRowExpansion(grid, row);
16371 }
16372 });
16373 grid.expandable.expandedAll = true;
16374 grid.queueGridRefresh();
16375 },
16376
16377 collapseAllRows: function(grid) {
16378 grid.renderContainers.body.visibleRowCache.forEach( function(row) {
16379 if (row.isExpanded) {
16380 service.toggleRowExpansion(grid, row);
16381 }
16382 });
16383 grid.expandable.expandedAll = false;
16384 grid.queueGridRefresh();
16385 },
16386
16387 toggleAllRows: function(grid) {
16388 if (grid.expandable.expandedAll) {
16389 service.collapseAllRows(grid);
16390 }
16391 else {
16392 service.expandAllRows(grid);
16393 }
16394 }
16395 };
16396 return service;
16397 }]);
16398
16399 /**
16400 * @ngdoc object
16401 * @name enableExpandableRowHeader
16402 * @propertyOf ui.grid.expandable.api:GridOptions
16403 * @description Show a rowHeader to provide the expandable buttons. If set to false then implies
16404 * you're going to use a custom method for expanding and collapsing the subgrids. Defaults to true.
16405 * @example
16406 * <pre>
16407 * $scope.gridOptions = {
16408 * enableExpandableRowHeader: false
16409 * }
16410 * </pre>
16411 */
16412 module.directive('uiGridExpandable', ['uiGridExpandableService', '$templateCache',
16413 function (uiGridExpandableService, $templateCache) {
16414 return {
16415 replace: true,
16416 priority: 0,
16417 require: '^uiGrid',
16418 scope: false,
16419 compile: function () {
16420 return {
16421 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
16422 if ( uiGridCtrl.grid.options.enableExpandableRowHeader !== false ) {
16423 var expandableRowHeaderColDef = {
16424 name: 'expandableButtons',
16425 displayName: '',
16426 exporterSuppressExport: true,
16427 enableColumnResizing: false,
16428 enableColumnMenu: false,
16429 width: uiGridCtrl.grid.options.expandableRowHeaderWidth || 40
16430 };
16431 expandableRowHeaderColDef.cellTemplate = $templateCache.get('ui-grid/expandableRowHeader');
16432 expandableRowHeaderColDef.headerCellTemplate = $templateCache.get('ui-grid/expandableTopRowHeader');
16433 uiGridCtrl.grid.addRowHeaderColumn(expandableRowHeaderColDef);
16434 }
16435 uiGridExpandableService.initializeGrid(uiGridCtrl.grid);
16436 },
16437 post: function ($scope, $elm, $attrs, uiGridCtrl) {
16438 }
16439 };
16440 }
16441 };
16442 }]);
16443
16444 /**
16445 * @ngdoc directive
16446 * @name ui.grid.expandable.directive:uiGrid
16447 * @description stacks on the uiGrid directive to register child grid with parent row when child is created
16448 */
16449 module.directive('uiGrid', ['uiGridExpandableService', '$templateCache',
16450 function (uiGridExpandableService, $templateCache) {
16451 return {
16452 replace: true,
16453 priority: 599,
16454 require: '^uiGrid',
16455 scope: false,
16456 compile: function () {
16457 return {
16458 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
16459
16460 uiGridCtrl.grid.api.core.on.renderingComplete($scope, function() {
16461 //if a parent grid row is on the scope, then add the parentRow property to this childGrid
16462 if ($scope.row && $scope.row.grid && $scope.row.grid.options && $scope.row.grid.options.enableExpandable) {
16463
16464 /**
16465 * @ngdoc directive
16466 * @name ui.grid.expandable.class:Grid
16467 * @description Additional Grid properties added by expandable module
16468 */
16469
16470 /**
16471 * @ngdoc object
16472 * @name parentRow
16473 * @propertyOf ui.grid.expandable.class:Grid
16474 * @description reference to the expanded parent row that owns this grid
16475 */
16476 uiGridCtrl.grid.parentRow = $scope.row;
16477
16478 //todo: adjust height on parent row when child grid height changes. we need some sort of gridHeightChanged event
16479 // uiGridCtrl.grid.core.on.canvasHeightChanged($scope, function(oldHeight, newHeight) {
16480 // uiGridCtrl.grid.parentRow = newHeight;
16481 // });
16482 }
16483
16484 });
16485 },
16486 post: function ($scope, $elm, $attrs, uiGridCtrl) {
16487
16488 }
16489 };
16490 }
16491 };
16492 }]);
16493
16494 /**
16495 * @ngdoc directive
16496 * @name ui.grid.expandable.directive:uiGridExpandableRow
16497 * @description directive to render the expandable row template
16498 */
16499 module.directive('uiGridExpandableRow',
16500 ['uiGridExpandableService', '$timeout', '$compile', 'uiGridConstants','gridUtil','$interval', '$log',
16501 function (uiGridExpandableService, $timeout, $compile, uiGridConstants, gridUtil, $interval, $log) {
16502
16503 return {
16504 replace: false,
16505 priority: 0,
16506 scope: false,
16507
16508 compile: function () {
16509 return {
16510 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
16511 gridUtil.getTemplate($scope.grid.options.expandableRowTemplate).then(
16512 function (template) {
16513 if ($scope.grid.options.expandableRowScope) {
16514 var expandableRowScope = $scope.grid.options.expandableRowScope;
16515 for (var property in expandableRowScope) {
16516 if (expandableRowScope.hasOwnProperty(property)) {
16517 $scope[property] = expandableRowScope[property];
16518 }
16519 }
16520 }
16521 var expandedRowElement = $compile(template)($scope);
16522 $elm.append(expandedRowElement);
16523 $scope.row.expandedRendered = true;
16524 });
16525 },
16526
16527 post: function ($scope, $elm, $attrs, uiGridCtrl) {
16528 $scope.$on('$destroy', function() {
16529 $scope.row.expandedRendered = false;
16530 });
16531 }
16532 };
16533 }
16534 };
16535 }]);
16536
16537 /**
16538 * @ngdoc directive
16539 * @name ui.grid.expandable.directive:uiGridRow
16540 * @description stacks on the uiGridRow directive to add support for expandable rows
16541 */
16542 module.directive('uiGridRow',
16543 ['$compile', 'gridUtil', '$templateCache',
16544 function ($compile, gridUtil, $templateCache) {
16545 return {
16546 priority: -200,
16547 scope: false,
16548 compile: function ($elm, $attrs) {
16549 return {
16550 pre: function ($scope, $elm, $attrs, controllers) {
16551
16552 $scope.expandableRow = {};
16553
16554 $scope.expandableRow.shouldRenderExpand = function () {
16555 var ret = $scope.colContainer.name === 'body' && $scope.grid.options.enableExpandable !== false && $scope.row.isExpanded && (!$scope.grid.isScrollingVertically || $scope.row.expandedRendered);
16556 return ret;
16557 };
16558
16559 $scope.expandableRow.shouldRenderFiller = function () {
16560 var ret = $scope.row.isExpanded && ( $scope.colContainer.name !== 'body' || ($scope.grid.isScrollingVertically && !$scope.row.expandedRendered));
16561 return ret;
16562 };
16563
16564 /*
16565 * Commented out @PaulL1. This has no purpose that I can see, and causes #2964. If this code needs to be reinstated for some
16566 * reason it needs to use drawnWidth, not width, and needs to check column visibility. It should really use render container
16567 * visible column cache also instead of checking column.renderContainer.
16568 function updateRowContainerWidth() {
16569 var grid = $scope.grid;
16570 var colWidth = 0;
16571 grid.columns.forEach( function (column) {
16572 if (column.renderContainer === 'left') {
16573 colWidth += column.width;
16574 }
16575 });
16576 colWidth = Math.floor(colWidth);
16577 return '.grid' + grid.id + ' .ui-grid-pinned-container-' + $scope.colContainer.name + ', .grid' + grid.id +
16578 ' .ui-grid-pinned-container-' + $scope.colContainer.name + ' .ui-grid-render-container-' + $scope.colContainer.name +
16579 ' .ui-grid-viewport .ui-grid-canvas .ui-grid-row { width: ' + colWidth + 'px; }';
16580 }
16581
16582 if ($scope.colContainer.name === 'left') {
16583 $scope.grid.registerStyleComputation({
16584 priority: 15,
16585 func: updateRowContainerWidth
16586 });
16587 }*/
16588
16589 },
16590 post: function ($scope, $elm, $attrs, controllers) {
16591 }
16592 };
16593 }
16594 };
16595 }]);
16596
16597 /**
16598 * @ngdoc directive
16599 * @name ui.grid.expandable.directive:uiGridViewport
16600 * @description stacks on the uiGridViewport directive to append the expandable row html elements to the
16601 * default gridRow template
16602 */
16603 module.directive('uiGridViewport',
16604 ['$compile', 'gridUtil', '$templateCache',
16605 function ($compile, gridUtil, $templateCache) {
16606 return {
16607 priority: -200,
16608 scope: false,
16609 compile: function ($elm, $attrs) {
16610 var rowRepeatDiv = angular.element($elm.children().children()[0]);
16611 var expandedRowFillerElement = $templateCache.get('ui-grid/expandableScrollFiller');
16612 var expandedRowElement = $templateCache.get('ui-grid/expandableRow');
16613 rowRepeatDiv.append(expandedRowElement);
16614 rowRepeatDiv.append(expandedRowFillerElement);
16615 return {
16616 pre: function ($scope, $elm, $attrs, controllers) {
16617 },
16618 post: function ($scope, $elm, $attrs, controllers) {
16619 }
16620 };
16621 }
16622 };
16623 }]);
16624
16625 })();
16626
16627 /* global console */
16628
16629 (function () {
16630 'use strict';
16631
16632 /**
16633 * @ngdoc overview
16634 * @name ui.grid.exporter
16635 * @description
16636 *
16637 * # ui.grid.exporter
16638 *
16639 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
16640 *
16641 * This module provides the ability to exporter data from the grid.
16642 *
16643 * Data can be exported in a range of formats, and all data, visible
16644 * data, or selected rows can be exported, with all columns or visible
16645 * columns.
16646 *
16647 * No UI is provided, the caller should provide their own UI/buttons
16648 * as appropriate, or enable the gridMenu
16649 *
16650 * <br/>
16651 * <br/>
16652 *
16653 * <div doc-module-components="ui.grid.exporter"></div>
16654 */
16655
16656 var module = angular.module('ui.grid.exporter', ['ui.grid']);
16657
16658 /**
16659 * @ngdoc object
16660 * @name ui.grid.exporter.constant:uiGridExporterConstants
16661 *
16662 * @description constants available in exporter module
16663 */
16664 /**
16665 * @ngdoc property
16666 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
16667 * @name ALL
16668 * @description export all data, including data not visible. Can
16669 * be set for either rowTypes or colTypes
16670 */
16671 /**
16672 * @ngdoc property
16673 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
16674 * @name VISIBLE
16675 * @description export only visible data, including data not visible. Can
16676 * be set for either rowTypes or colTypes
16677 */
16678 /**
16679 * @ngdoc property
16680 * @propertyOf ui.grid.exporter.constant:uiGridExporterConstants
16681 * @name SELECTED
16682 * @description export all data, including data not visible. Can
16683 * be set only for rowTypes, selection of only some columns is
16684 * not supported
16685 */
16686 module.constant('uiGridExporterConstants', {
16687 featureName: 'exporter',
16688 ALL: 'all',
16689 VISIBLE: 'visible',
16690 SELECTED: 'selected',
16691 CSV_CONTENT: 'CSV_CONTENT',
16692 BUTTON_LABEL: 'BUTTON_LABEL',
16693 FILE_NAME: 'FILE_NAME'
16694 });
16695
16696 /**
16697 * @ngdoc service
16698 * @name ui.grid.exporter.service:uiGridExporterService
16699 *
16700 * @description Services for exporter feature
16701 */
16702 module.service('uiGridExporterService', ['$q', 'uiGridExporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService',
16703 function ($q, uiGridExporterConstants, gridUtil, $compile, $interval, i18nService) {
16704
16705 var service = {
16706
16707 delay: 100,
16708
16709 initializeGrid: function (grid) {
16710
16711 //add feature namespace and any properties to grid for needed state
16712 grid.exporter = {};
16713 this.defaultGridOptions(grid.options);
16714
16715 /**
16716 * @ngdoc object
16717 * @name ui.grid.exporter.api:PublicApi
16718 *
16719 * @description Public Api for exporter feature
16720 */
16721 var publicApi = {
16722 events: {
16723 exporter: {
16724 }
16725 },
16726 methods: {
16727 exporter: {
16728 /**
16729 * @ngdoc function
16730 * @name csvExport
16731 * @methodOf ui.grid.exporter.api:PublicApi
16732 * @description Exports rows from the grid in csv format,
16733 * the data exported is selected based on the provided options
16734 * @param {string} rowTypes which rows to export, valid values are
16735 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
16736 * uiGridExporterConstants.SELECTED
16737 * @param {string} colTypes which columns to export, valid values are
16738 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
16739 */
16740 csvExport: function (rowTypes, colTypes) {
16741 service.csvExport(grid, rowTypes, colTypes);
16742 },
16743 /**
16744 * @ngdoc function
16745 * @name pdfExport
16746 * @methodOf ui.grid.exporter.api:PublicApi
16747 * @description Exports rows from the grid in pdf format,
16748 * the data exported is selected based on the provided options
16749 * Note that this function has a dependency on pdfMake, all
16750 * going well this has been installed for you.
16751 * The resulting pdf opens in a new browser window.
16752 * @param {string} rowTypes which rows to export, valid values are
16753 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
16754 * uiGridExporterConstants.SELECTED
16755 * @param {string} colTypes which columns to export, valid values are
16756 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE
16757 */
16758 pdfExport: function (rowTypes, colTypes) {
16759 service.pdfExport(grid, rowTypes, colTypes);
16760 }
16761 }
16762 }
16763 };
16764
16765 grid.api.registerEventsFromObject(publicApi.events);
16766
16767 grid.api.registerMethodsFromObject(publicApi.methods);
16768
16769 if (grid.api.core.addToGridMenu){
16770 service.addToMenu( grid );
16771 } else {
16772 // order of registration is not guaranteed, register in a little while
16773 $interval( function() {
16774 if (grid.api.core.addToGridMenu){
16775 service.addToMenu( grid );
16776 }
16777 }, this.delay, 1);
16778 }
16779
16780 },
16781
16782 defaultGridOptions: function (gridOptions) {
16783 //default option to true unless it was explicitly set to false
16784 /**
16785 * @ngdoc object
16786 * @name ui.grid.exporter.api:GridOptions
16787 *
16788 * @description GridOptions for exporter feature, these are available to be
16789 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
16790 */
16791 /**
16792 * @ngdoc object
16793 * @name ui.grid.exporter.api:ColumnDef
16794 * @description ColumnDef settings for exporter
16795 */
16796 /**
16797 * @ngdoc object
16798 * @name exporterSuppressMenu
16799 * @propertyOf ui.grid.exporter.api:GridOptions
16800 * @description Don't show the export menu button, implying the user
16801 * will roll their own UI for calling the exporter
16802 * <br/>Defaults to false
16803 */
16804 gridOptions.exporterSuppressMenu = gridOptions.exporterSuppressMenu === true;
16805 /**
16806 * @ngdoc object
16807 * @name exporterMenuLabel
16808 * @propertyOf ui.grid.exporter.api:GridOptions
16809 * @description The text to show on the exporter menu button
16810 * link
16811 * <br/>Defaults to 'Export'
16812 */
16813 gridOptions.exporterMenuLabel = gridOptions.exporterMenuLabel ? gridOptions.exporterMenuLabel : 'Export';
16814 /**
16815 * @ngdoc object
16816 * @name exporterSuppressColumns
16817 * @propertyOf ui.grid.exporter.api:GridOptions
16818 * @description Columns that should not be exported. The selectionRowHeader is already automatically
16819 * suppressed, but if you had a button column or some other "system" column that shouldn't be shown in the
16820 * output then add it in this list. You should provide an array of column names.
16821 * <br/>Defaults to: []
16822 * <pre>
16823 * gridOptions.exporterSuppressColumns = [ 'buttons' ];
16824 * </pre>
16825 */
16826 gridOptions.exporterSuppressColumns = gridOptions.exporterSuppressColumns ? gridOptions.exporterSuppressColumns : [];
16827 /**
16828 * @ngdoc object
16829 * @name exporterCsvColumnSeparator
16830 * @propertyOf ui.grid.exporter.api:GridOptions
16831 * @description The character to use as column separator
16832 * link
16833 * <br/>Defaults to ','
16834 */
16835 gridOptions.exporterCsvColumnSeparator = gridOptions.exporterCsvColumnSeparator ? gridOptions.exporterCsvColumnSeparator : ',';
16836 /**
16837 * @ngdoc object
16838 * @name exporterCsvFilename
16839 * @propertyOf ui.grid.exporter.api:GridOptions
16840 * @description The default filename to use when saving the downloaded csv.
16841 * This will only work in some browsers.
16842 * <br/>Defaults to 'download.csv'
16843 */
16844 gridOptions.exporterCsvFilename = gridOptions.exporterCsvFilename ? gridOptions.exporterCsvFilename : 'download.csv';
16845 /**
16846 * @ngdoc object
16847 * @name exporterPdfFilename
16848 * @propertyOf ui.grid.exporter.api:GridOptions
16849 * @description The default filename to use when saving the downloaded pdf, only used in IE (other browsers open pdfs in a new window)
16850 * <br/>Defaults to 'download.pdf'
16851 */
16852 gridOptions.exporterPdfFilename = gridOptions.exporterPdfFilename ? gridOptions.exporterPdfFilename : 'download.pdf';
16853 /**
16854 * @ngdoc object
16855 * @name exporterOlderExcelCompatibility
16856 * @propertyOf ui.grid.exporter.api:GridOptions
16857 * @description Some versions of excel don't like the utf-16 BOM on the front, and it comes
16858 * through as  in the first column header. Setting this option to false will suppress this, at the
16859 * expense of proper utf-16 handling in applications that do recognise the BOM
16860 * <br/>Defaults to false
16861 */
16862 gridOptions.exporterOlderExcelCompatibility = gridOptions.exporterOlderExcelCompatibility === true;
16863 /**
16864 * @ngdoc object
16865 * @name exporterPdfDefaultStyle
16866 * @propertyOf ui.grid.exporter.api:GridOptions
16867 * @description The default style in pdfMake format
16868 * <br/>Defaults to:
16869 * <pre>
16870 * {
16871 * fontSize: 11
16872 * }
16873 * </pre>
16874 */
16875 gridOptions.exporterPdfDefaultStyle = gridOptions.exporterPdfDefaultStyle ? gridOptions.exporterPdfDefaultStyle : { fontSize: 11 };
16876 /**
16877 * @ngdoc object
16878 * @name exporterPdfTableStyle
16879 * @propertyOf ui.grid.exporter.api:GridOptions
16880 * @description The table style in pdfMake format
16881 * <br/>Defaults to:
16882 * <pre>
16883 * {
16884 * margin: [0, 5, 0, 15]
16885 * }
16886 * </pre>
16887 */
16888 gridOptions.exporterPdfTableStyle = gridOptions.exporterPdfTableStyle ? gridOptions.exporterPdfTableStyle : { margin: [0, 5, 0, 15] };
16889 /**
16890 * @ngdoc object
16891 * @name exporterPdfTableHeaderStyle
16892 * @propertyOf ui.grid.exporter.api:GridOptions
16893 * @description The tableHeader style in pdfMake format
16894 * <br/>Defaults to:
16895 * <pre>
16896 * {
16897 * bold: true,
16898 * fontSize: 12,
16899 * color: 'black'
16900 * }
16901 * </pre>
16902 */
16903 gridOptions.exporterPdfTableHeaderStyle = gridOptions.exporterPdfTableHeaderStyle ? gridOptions.exporterPdfTableHeaderStyle : { bold: true, fontSize: 12, color: 'black' };
16904 /**
16905 * @ngdoc object
16906 * @name exporterPdfHeader
16907 * @propertyOf ui.grid.exporter.api:GridOptions
16908 * @description The header section for pdf exports. Can be
16909 * simple text:
16910 * <pre>
16911 * gridOptions.exporterPdfHeader = 'My Header';
16912 * </pre>
16913 * Can be a more complex object in pdfMake format:
16914 * <pre>
16915 * gridOptions.exporterPdfHeader = {
16916 * columns: [
16917 * 'Left part',
16918 * { text: 'Right part', alignment: 'right' }
16919 * ]
16920 * };
16921 * </pre>
16922 * Or can be a function, allowing page numbers and the like
16923 * <pre>
16924 * gridOptions.exporterPdfHeader: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
16925 * </pre>
16926 */
16927 gridOptions.exporterPdfHeader = gridOptions.exporterPdfHeader ? gridOptions.exporterPdfHeader : null;
16928 /**
16929 * @ngdoc object
16930 * @name exporterPdfFooter
16931 * @propertyOf ui.grid.exporter.api:GridOptions
16932 * @description The header section for pdf exports. Can be
16933 * simple text:
16934 * <pre>
16935 * gridOptions.exporterPdfFooter = 'My Footer';
16936 * </pre>
16937 * Can be a more complex object in pdfMake format:
16938 * <pre>
16939 * gridOptions.exporterPdfFooter = {
16940 * columns: [
16941 * 'Left part',
16942 * { text: 'Right part', alignment: 'right' }
16943 * ]
16944 * };
16945 * </pre>
16946 * Or can be a function, allowing page numbers and the like
16947 * <pre>
16948 * gridOptions.exporterPdfFooter: function(currentPage, pageCount) { return currentPage.toString() + ' of ' + pageCount; };
16949 * </pre>
16950 */
16951 gridOptions.exporterPdfFooter = gridOptions.exporterPdfFooter ? gridOptions.exporterPdfFooter : null;
16952 /**
16953 * @ngdoc object
16954 * @name exporterPdfOrientation
16955 * @propertyOf ui.grid.exporter.api:GridOptions
16956 * @description The orientation, should be a valid pdfMake value,
16957 * 'landscape' or 'portrait'
16958 * <br/>Defaults to landscape
16959 */
16960 gridOptions.exporterPdfOrientation = gridOptions.exporterPdfOrientation ? gridOptions.exporterPdfOrientation : 'landscape';
16961 /**
16962 * @ngdoc object
16963 * @name exporterPdfPageSize
16964 * @propertyOf ui.grid.exporter.api:GridOptions
16965 * @description The orientation, should be a valid pdfMake
16966 * paper size, usually 'A4' or 'LETTER'
16967 * {@link https://github.com/bpampuch/pdfmake/blob/master/src/standardPageSizes.js pdfMake page sizes}
16968 * <br/>Defaults to A4
16969 */
16970 gridOptions.exporterPdfPageSize = gridOptions.exporterPdfPageSize ? gridOptions.exporterPdfPageSize : 'A4';
16971 /**
16972 * @ngdoc object
16973 * @name exporterPdfMaxGridWidth
16974 * @propertyOf ui.grid.exporter.api:GridOptions
16975 * @description The maxium grid width - the current grid width
16976 * will be scaled to match this, with any fixed width columns
16977 * being adjusted accordingly.
16978 * <br/>Defaults to 720 (for A4 landscape), use 670 for LETTER
16979 */
16980 gridOptions.exporterPdfMaxGridWidth = gridOptions.exporterPdfMaxGridWidth ? gridOptions.exporterPdfMaxGridWidth : 720;
16981 /**
16982 * @ngdoc object
16983 * @name exporterPdfTableLayout
16984 * @propertyOf ui.grid.exporter.api:GridOptions
16985 * @description A tableLayout in pdfMake format,
16986 * controls gridlines and the like. We use the default
16987 * layout usually.
16988 * <br/>Defaults to null, which means no layout
16989 */
16990
16991 /**
16992 * @ngdoc object
16993 * @name exporterMenuAllData
16994 * @porpertyOf ui.grid.exporter.api:GridOptions
16995 * @description Add export all data as cvs/pdf menu items to the ui-grid grid menu, if it's present. Defaults to true.
16996 */
16997 gridOptions.exporterMenuAllData = gridOptions.exporterMenuAllData !== undefined ? gridOptions.exporterMenuAllData : true;
16998
16999 /**
17000 * @ngdoc object
17001 * @name exporterMenuCsv
17002 * @propertyOf ui.grid.exporter.api:GridOptions
17003 * @description Add csv export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17004 */
17005 gridOptions.exporterMenuCsv = gridOptions.exporterMenuCsv !== undefined ? gridOptions.exporterMenuCsv : true;
17006
17007 /**
17008 * @ngdoc object
17009 * @name exporterMenuPdf
17010 * @propertyOf ui.grid.exporter.api:GridOptions
17011 * @description Add pdf export menu items to the ui-grid grid menu, if it's present. Defaults to true.
17012 */
17013 gridOptions.exporterMenuPdf = gridOptions.exporterMenuPdf !== undefined ? gridOptions.exporterMenuPdf : true;
17014
17015 /**
17016 * @ngdoc object
17017 * @name exporterPdfCustomFormatter
17018 * @propertyOf ui.grid.exporter.api:GridOptions
17019 * @description A custom callback routine that changes the pdf document, adding any
17020 * custom styling or content that is supported by pdfMake. Takes in the complete docDefinition, and
17021 * must return an updated docDefinition ready for pdfMake.
17022 * @example
17023 * In this example we add a style to the style array, so that we can use it in our
17024 * footer definition.
17025 * <pre>
17026 * gridOptions.exporterPdfCustomFormatter = function ( docDefinition ) {
17027 * docDefinition.styles.footerStyle = { bold: true, fontSize: 10 };
17028 * return docDefinition;
17029 * }
17030 *
17031 * gridOptions.exporterPdfFooter = { text: 'My footer', style: 'footerStyle' }
17032 * </pre>
17033 */
17034 gridOptions.exporterPdfCustomFormatter = ( gridOptions.exporterPdfCustomFormatter && typeof( gridOptions.exporterPdfCustomFormatter ) === 'function' ) ? gridOptions.exporterPdfCustomFormatter : function ( docDef ) { return docDef; };
17035
17036 /**
17037 * @ngdoc object
17038 * @name exporterHeaderFilterUseName
17039 * @propertyOf ui.grid.exporter.api:GridOptions
17040 * @description Defaults to false, which leads to `displayName` being passed into the headerFilter.
17041 * If set to true, then will pass `name` instead.
17042 *
17043 *
17044 * @example
17045 * <pre>
17046 * gridOptions.exporterHeaderFilterUseName = true;
17047 * </pre>
17048 */
17049 gridOptions.exporterHeaderFilterUseName = gridOptions.exporterHeaderFilterUseName === true;
17050
17051 /**
17052 * @ngdoc object
17053 * @name exporterHeaderFilter
17054 * @propertyOf ui.grid.exporter.api:GridOptions
17055 * @description A function to apply to the header displayNames before exporting. Useful for internationalisation,
17056 * for example if you were using angular-translate you'd set this to `$translate.instant`. Note that this
17057 * call must be synchronous, it cannot be a call that returns a promise.
17058 *
17059 * Behaviour can be changed to pass in `name` instead of `displayName` through use of `exporterHeaderFilterUseName: true`.
17060 *
17061 * @example
17062 * <pre>
17063 * gridOptions.exporterHeaderFilter = function( displayName ){ return 'col: ' + name; };
17064 * </pre>
17065 * OR
17066 * <pre>
17067 * gridOptions.exporterHeaderFilter = $translate.instant;
17068 * </pre>
17069 */
17070
17071 /**
17072 * @ngdoc function
17073 * @name exporterFieldCallback
17074 * @propertyOf ui.grid.exporter.api:GridOptions
17075 * @description A function to call for each field before exporting it. Allows
17076 * massaging of raw data into a display format, for example if you have applied
17077 * filters to convert codes into decodes, or you require
17078 * a specific date format in the exported content.
17079 *
17080 * The method is called once for each field exported, and provides the grid, the
17081 * gridCol and the GridRow for you to use as context in massaging the data.
17082 *
17083 * @param {Grid} grid provides the grid in case you have need of it
17084 * @param {GridRow} row the row from which the data comes
17085 * @param {GridCol} col the column from which the data comes
17086 * @param {object} value the value for your massaging
17087 * @returns {object} you must return the massaged value ready for exporting
17088 *
17089 * @example
17090 * <pre>
17091 * gridOptions.exporterFieldCallback = function ( grid, row, col, value ){
17092 * if ( col.name === 'status' ){
17093 * value = decodeStatus( value );
17094 * }
17095 * return value;
17096 * }
17097 * </pre>
17098 */
17099 gridOptions.exporterFieldCallback = gridOptions.exporterFieldCallback ? gridOptions.exporterFieldCallback : function( grid, row, col, value ) { return value; };
17100
17101 /**
17102 * @ngdoc function
17103 * @name exporterAllDataFn
17104 * @propertyOf ui.grid.exporter.api:GridOptions
17105 * @description This promise is needed when exporting all rows,
17106 * and the data need to be provided by server side. Default is null.
17107 * @returns {Promise} a promise to load all data from server
17108 *
17109 * @example
17110 * <pre>
17111 * gridOptions.exporterAllDataFn = function () {
17112 * return $http.get('/data/100.json')
17113 * }
17114 * </pre>
17115 */
17116 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataFn ? gridOptions.exporterAllDataFn : null;
17117
17118 /**
17119 * @ngdoc function
17120 * @name exporterAllDataPromise
17121 * @propertyOf ui.grid.exporter.api:GridOptions
17122 * @description DEPRECATED - exporterAllDataFn used to be
17123 * called this, but it wasn't a promise, it was a function that returned
17124 * a promise. Deprecated, but supported for backward compatibility, use
17125 * exporterAllDataFn instead.
17126 * @returns {Promise} a promise to load all data from server
17127 *
17128 * @example
17129 * <pre>
17130 * gridOptions.exporterAllDataFn = function () {
17131 * return $http.get('/data/100.json')
17132 * }
17133 * </pre>
17134 */
17135 if ( gridOptions.exporterAllDataFn == null && gridOptions.exporterAllDataPromise ) {
17136 gridOptions.exporterAllDataFn = gridOptions.exporterAllDataPromise;
17137 }
17138 },
17139
17140
17141 /**
17142 * @ngdoc function
17143 * @name addToMenu
17144 * @methodOf ui.grid.exporter.service:uiGridExporterService
17145 * @description Adds export items to the grid menu,
17146 * allowing the user to select export options
17147 * @param {Grid} grid the grid from which data should be exported
17148 */
17149 addToMenu: function ( grid ) {
17150 grid.api.core.addToGridMenu( grid, [
17151 {
17152 title: i18nService.getSafeText('gridMenu.exporterAllAsCsv'),
17153 action: function ($event) {
17154 this.grid.api.exporter.csvExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17155 },
17156 shown: function() {
17157 return this.grid.options.exporterMenuCsv && this.grid.options.exporterMenuAllData;
17158 },
17159 order: 200
17160 },
17161 {
17162 title: i18nService.getSafeText('gridMenu.exporterVisibleAsCsv'),
17163 action: function ($event) {
17164 this.grid.api.exporter.csvExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17165 },
17166 shown: function() {
17167 return this.grid.options.exporterMenuCsv;
17168 },
17169 order: 201
17170 },
17171 {
17172 title: i18nService.getSafeText('gridMenu.exporterSelectedAsCsv'),
17173 action: function ($event) {
17174 this.grid.api.exporter.csvExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17175 },
17176 shown: function() {
17177 return this.grid.options.exporterMenuCsv &&
17178 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17179 },
17180 order: 202
17181 },
17182 {
17183 title: i18nService.getSafeText('gridMenu.exporterAllAsPdf'),
17184 action: function ($event) {
17185 this.grid.api.exporter.pdfExport( uiGridExporterConstants.ALL, uiGridExporterConstants.ALL );
17186 },
17187 shown: function() {
17188 return this.grid.options.exporterMenuPdf && this.grid.options.exporterMenuAllData;
17189 },
17190 order: 203
17191 },
17192 {
17193 title: i18nService.getSafeText('gridMenu.exporterVisibleAsPdf'),
17194 action: function ($event) {
17195 this.grid.api.exporter.pdfExport( uiGridExporterConstants.VISIBLE, uiGridExporterConstants.VISIBLE );
17196 },
17197 shown: function() {
17198 return this.grid.options.exporterMenuPdf;
17199 },
17200 order: 204
17201 },
17202 {
17203 title: i18nService.getSafeText('gridMenu.exporterSelectedAsPdf'),
17204 action: function ($event) {
17205 this.grid.api.exporter.pdfExport( uiGridExporterConstants.SELECTED, uiGridExporterConstants.VISIBLE );
17206 },
17207 shown: function() {
17208 return this.grid.options.exporterMenuPdf &&
17209 ( this.grid.api.selection && this.grid.api.selection.getSelectedRows().length > 0 );
17210 },
17211 order: 205
17212 }
17213 ]);
17214 },
17215
17216
17217 /**
17218 * @ngdoc function
17219 * @name csvExport
17220 * @methodOf ui.grid.exporter.service:uiGridExporterService
17221 * @description Exports rows from the grid in csv format,
17222 * the data exported is selected based on the provided options
17223 * @param {Grid} grid the grid from which data should be exported
17224 * @param {string} rowTypes which rows to export, valid values are
17225 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17226 * uiGridExporterConstants.SELECTED
17227 * @param {string} colTypes which columns to export, valid values are
17228 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17229 * uiGridExporterConstants.SELECTED
17230 */
17231 csvExport: function (grid, rowTypes, colTypes) {
17232 var self = this;
17233 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function() {
17234 var exportColumnHeaders = grid.options.showHeader ? self.getColumnHeaders(grid, colTypes) : [];
17235 var exportData = self.getData(grid, rowTypes, colTypes);
17236 var csvContent = self.formatAsCsv(exportColumnHeaders, exportData, grid.options.exporterCsvColumnSeparator);
17237
17238 self.downloadFile (grid.options.exporterCsvFilename, csvContent, grid.options.exporterOlderExcelCompatibility);
17239 });
17240 },
17241
17242 /**
17243 * @ngdoc function
17244 * @name loadAllDataIfNeeded
17245 * @methodOf ui.grid.exporter.service:uiGridExporterService
17246 * @description When using server side pagination, use exporterAllDataFn to
17247 * load all data before continuing processing.
17248 * When using client side pagination, return a resolved promise so processing
17249 * continues immediately
17250 * @param {Grid} grid the grid from which data should be exported
17251 * @param {string} rowTypes which rows to export, valid values are
17252 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17253 * uiGridExporterConstants.SELECTED
17254 * @param {string} colTypes which columns to export, valid values are
17255 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17256 * uiGridExporterConstants.SELECTED
17257 */
17258 loadAllDataIfNeeded: function (grid, rowTypes, colTypes) {
17259 if ( rowTypes === uiGridExporterConstants.ALL && grid.rows.length !== grid.options.totalItems && grid.options.exporterAllDataFn) {
17260 return grid.options.exporterAllDataFn()
17261 .then(function() {
17262 grid.modifyRows(grid.options.data);
17263 });
17264 } else {
17265 var deferred = $q.defer();
17266 deferred.resolve();
17267 return deferred.promise;
17268 }
17269 },
17270
17271 /**
17272 * @ngdoc property
17273 * @propertyOf ui.grid.exporter.api:ColumnDef
17274 * @name exporterSuppressExport
17275 * @description Suppresses export for this column. Used by selection and expandable.
17276 */
17277
17278 /**
17279 * @ngdoc function
17280 * @name getColumnHeaders
17281 * @methodOf ui.grid.exporter.service:uiGridExporterService
17282 * @description Gets the column headers from the grid to use
17283 * as a title row for the exported file, all headers have
17284 * headerCellFilters applied as appropriate.
17285 *
17286 * Column headers are an array of objects, each object has
17287 * name, displayName, width and align attributes. Only name is
17288 * used for csv, all attributes are used for pdf.
17289 *
17290 * @param {Grid} grid the grid from which data should be exported
17291 * @param {string} colTypes which columns to export, valid values are
17292 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17293 * uiGridExporterConstants.SELECTED
17294 */
17295 getColumnHeaders: function (grid, colTypes) {
17296 var headers = [];
17297 var columns;
17298
17299 if ( colTypes === uiGridExporterConstants.ALL ){
17300 columns = grid.columns;
17301 } else {
17302 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17303 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17304 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17305
17306 columns = leftColumns.concat(bodyColumns,rightColumns);
17307 }
17308
17309 columns.forEach( function( gridCol, index ) {
17310 if ( gridCol.colDef.exporterSuppressExport !== true &&
17311 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
17312 headers.push({
17313 name: gridCol.field,
17314 displayName: grid.options.exporterHeaderFilter ? ( grid.options.exporterHeaderFilterUseName ? grid.options.exporterHeaderFilter(gridCol.name) : grid.options.exporterHeaderFilter(gridCol.displayName) ) : gridCol.displayName,
17315 width: gridCol.drawnWidth ? gridCol.drawnWidth : gridCol.width,
17316 align: gridCol.colDef.type === 'number' ? 'right' : 'left'
17317 });
17318 }
17319 });
17320
17321 return headers;
17322 },
17323
17324
17325 /**
17326 * @ngdoc property
17327 * @propertyOf ui.grid.exporter.api:ColumnDef
17328 * @name exporterPdfAlign
17329 * @description the alignment you'd like for this specific column when
17330 * exported into a pdf. Can be 'left', 'right', 'center' or any other
17331 * valid pdfMake alignment option.
17332 */
17333
17334
17335 /**
17336 * @ngdoc object
17337 * @name ui.grid.exporter.api:GridRow
17338 * @description GridRow settings for exporter
17339 */
17340 /**
17341 * @ngdoc object
17342 * @name exporterEnableExporting
17343 * @propertyOf ui.grid.exporter.api:GridRow
17344 * @description If set to false, then don't export this row, notwithstanding visible or
17345 * other settings
17346 * <br/>Defaults to true
17347 */
17348
17349 /**
17350 * @ngdoc function
17351 * @name getData
17352 * @methodOf ui.grid.exporter.service:uiGridExporterService
17353 * @description Gets data from the grid based on the provided options,
17354 * all cells have cellFilters applied as appropriate. Any rows marked
17355 * `exporterEnableExporting: false` will not be exported
17356 * @param {Grid} grid the grid from which data should be exported
17357 * @param {string} rowTypes which rows to export, valid values are
17358 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17359 * uiGridExporterConstants.SELECTED
17360 * @param {string} colTypes which columns to export, valid values are
17361 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17362 * uiGridExporterConstants.SELECTED
17363 */
17364 getData: function (grid, rowTypes, colTypes) {
17365 var data = [];
17366 var rows;
17367 var columns;
17368
17369 switch ( rowTypes ) {
17370 case uiGridExporterConstants.ALL:
17371 rows = grid.rows;
17372 break;
17373 case uiGridExporterConstants.VISIBLE:
17374 rows = grid.getVisibleRows();
17375 break;
17376 case uiGridExporterConstants.SELECTED:
17377 if ( grid.api.selection ){
17378 rows = grid.api.selection.getSelectedGridRows();
17379 } else {
17380 gridUtil.logError('selection feature must be enabled to allow selected rows to be exported');
17381 }
17382 break;
17383 }
17384
17385 if ( colTypes === uiGridExporterConstants.ALL ){
17386 columns = grid.columns;
17387 } else {
17388 var leftColumns = grid.renderContainers.left ? grid.renderContainers.left.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17389 var bodyColumns = grid.renderContainers.body ? grid.renderContainers.body.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17390 var rightColumns = grid.renderContainers.right ? grid.renderContainers.right.visibleColumnCache.filter( function( column ){ return column.visible; } ) : [];
17391
17392 columns = leftColumns.concat(bodyColumns,rightColumns);
17393 }
17394
17395 rows.forEach( function( row, index ) {
17396
17397 if (row.exporterEnableExporting !== false) {
17398 var extractedRow = [];
17399
17400
17401 columns.forEach( function( gridCol, index ) {
17402 if ( (gridCol.visible || colTypes === uiGridExporterConstants.ALL ) &&
17403 gridCol.colDef.exporterSuppressExport !== true &&
17404 grid.options.exporterSuppressColumns.indexOf( gridCol.name ) === -1 ){
17405 var extractedField = { value: grid.options.exporterFieldCallback( grid, row, gridCol, grid.getCellValue( row, gridCol ) ) };
17406 if ( gridCol.colDef.exporterPdfAlign ) {
17407 extractedField.alignment = gridCol.colDef.exporterPdfAlign;
17408 }
17409 extractedRow.push(extractedField);
17410 }
17411 });
17412
17413 data.push(extractedRow);
17414 }
17415 });
17416
17417 return data;
17418 },
17419
17420
17421 /**
17422 * @ngdoc function
17423 * @name formatAsCSV
17424 * @methodOf ui.grid.exporter.service:uiGridExporterService
17425 * @description Formats the column headers and data as a CSV,
17426 * and sends that data to the user
17427 * @param {array} exportColumnHeaders an array of column headers,
17428 * where each header is an object with name, width and maybe alignment
17429 * @param {array} exportData an array of rows, where each row is
17430 * an array of column data
17431 * @returns {string} csv the formatted csv as a string
17432 */
17433 formatAsCsv: function (exportColumnHeaders, exportData, separator) {
17434 var self = this;
17435
17436 var bareHeaders = exportColumnHeaders.map(function(header){return { value: header.displayName };});
17437
17438 var csv = bareHeaders.length > 0 ? (self.formatRowAsCsv(this, separator)(bareHeaders) + '\n') : '';
17439
17440 csv += exportData.map(this.formatRowAsCsv(this, separator)).join('\n');
17441
17442 return csv;
17443 },
17444
17445 /**
17446 * @ngdoc function
17447 * @name formatRowAsCsv
17448 * @methodOf ui.grid.exporter.service:uiGridExporterService
17449 * @description Renders a single field as a csv field, including
17450 * quotes around the value
17451 * @param {exporterService} exporter pass in exporter
17452 * @param {array} row the row to be turned into a csv string
17453 * @returns {string} a csv-ified version of the row
17454 */
17455 formatRowAsCsv: function (exporter, separator) {
17456 return function (row) {
17457 return row.map(exporter.formatFieldAsCsv).join(separator);
17458 };
17459 },
17460
17461 /**
17462 * @ngdoc function
17463 * @name formatFieldAsCsv
17464 * @methodOf ui.grid.exporter.service:uiGridExporterService
17465 * @description Renders a single field as a csv field, including
17466 * quotes around the value
17467 * @param {field} field the field to be turned into a csv string,
17468 * may be of any type
17469 * @returns {string} a csv-ified version of the field
17470 */
17471 formatFieldAsCsv: function (field) {
17472 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
17473 return '';
17474 }
17475 if (typeof(field.value) === 'number') {
17476 return field.value;
17477 }
17478 if (typeof(field.value) === 'boolean') {
17479 return (field.value ? 'TRUE' : 'FALSE') ;
17480 }
17481 if (typeof(field.value) === 'string') {
17482 return '"' + field.value.replace(/"/g,'""') + '"';
17483 }
17484
17485 return JSON.stringify(field.value);
17486 },
17487
17488
17489 /**
17490 * @ngdoc function
17491 * @name isIE
17492 * @methodOf ui.grid.exporter.service:uiGridExporterService
17493 * @description Checks whether current browser is IE and returns it's version if it is
17494 */
17495 isIE: function () {
17496 var match = navigator.userAgent.match(/(?:MSIE |Trident\/.*; rv:)(\d+)/);
17497 return match ? parseInt(match[1]) : false;
17498 },
17499
17500
17501 /**
17502 * @ngdoc function
17503 * @name downloadFile
17504 * @methodOf ui.grid.exporter.service:uiGridExporterService
17505 * @description Triggers download of a csv file. Logic provided
17506 * by @cssensei (from his colleagues at https://github.com/ifeelgoods) in issue #2391
17507 * @param {string} fileName the filename we'd like our file to be
17508 * given
17509 * @param {string} csvContent the csv content that we'd like to
17510 * download as a file
17511 * @param {boolean} exporterOlderExcelCompatibility whether or not we put a utf-16 BOM on the from (\uFEFF)
17512 */
17513 downloadFile: function (fileName, csvContent, exporterOlderExcelCompatibility) {
17514 var D = document;
17515 var a = D.createElement('a');
17516 var strMimeType = 'application/octet-stream;charset=utf-8';
17517 var rawFile;
17518 var ieVersion;
17519
17520 ieVersion = this.isIE();
17521 if (ieVersion && ieVersion < 10) {
17522 var frame = D.createElement('iframe');
17523 document.body.appendChild(frame);
17524
17525 frame.contentWindow.document.open("text/html", "replace");
17526 frame.contentWindow.document.write('sep=,\r\n' + csvContent);
17527 frame.contentWindow.document.close();
17528 frame.contentWindow.focus();
17529 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
17530
17531 document.body.removeChild(frame);
17532 return true;
17533 }
17534
17535 // IE10+
17536 if (navigator.msSaveBlob) {
17537 return navigator.msSaveOrOpenBlob(
17538 new Blob(
17539 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
17540 { type: strMimeType } ),
17541 fileName
17542 );
17543 }
17544
17545 //html5 A[download]
17546 if ('download' in a) {
17547 var blob = new Blob(
17548 [exporterOlderExcelCompatibility ? "\uFEFF" : '', csvContent],
17549 { type: strMimeType }
17550 );
17551 rawFile = URL.createObjectURL(blob);
17552 a.setAttribute('download', fileName);
17553 } else {
17554 rawFile = 'data:' + strMimeType + ',' + encodeURIComponent(csvContent);
17555 a.setAttribute('target', '_blank');
17556 }
17557
17558 a.href = rawFile;
17559 a.setAttribute('style', 'display:none;');
17560 D.body.appendChild(a);
17561 setTimeout(function() {
17562 if (a.click) {
17563 a.click();
17564 // Workaround for Safari 5
17565 } else if (document.createEvent) {
17566 var eventObj = document.createEvent('MouseEvents');
17567 eventObj.initEvent('click', true, true);
17568 a.dispatchEvent(eventObj);
17569 }
17570 D.body.removeChild(a);
17571
17572 }, this.delay);
17573 },
17574
17575 /**
17576 * @ngdoc function
17577 * @name pdfExport
17578 * @methodOf ui.grid.exporter.service:uiGridExporterService
17579 * @description Exports rows from the grid in pdf format,
17580 * the data exported is selected based on the provided options.
17581 * Note that this function has a dependency on pdfMake, which must
17582 * be installed. The resulting pdf opens in a new
17583 * browser window.
17584 * @param {Grid} grid the grid from which data should be exported
17585 * @param {string} rowTypes which rows to export, valid values are
17586 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17587 * uiGridExporterConstants.SELECTED
17588 * @param {string} colTypes which columns to export, valid values are
17589 * uiGridExporterConstants.ALL, uiGridExporterConstants.VISIBLE,
17590 * uiGridExporterConstants.SELECTED
17591 */
17592 pdfExport: function (grid, rowTypes, colTypes) {
17593 var self = this;
17594 this.loadAllDataIfNeeded(grid, rowTypes, colTypes).then(function () {
17595 var exportColumnHeaders = self.getColumnHeaders(grid, colTypes);
17596 var exportData = self.getData(grid, rowTypes, colTypes);
17597 var docDefinition = self.prepareAsPdf(grid, exportColumnHeaders, exportData);
17598
17599 if (self.isIE()) {
17600 self.downloadPDF(grid.options.exporterPdfFilename, docDefinition);
17601 } else {
17602 pdfMake.createPdf(docDefinition).open();
17603 }
17604 });
17605 },
17606
17607
17608 /**
17609 * @ngdoc function
17610 * @name downloadPdf
17611 * @methodOf ui.grid.exporter.service:uiGridExporterService
17612 * @description Generates and retrieves the pdf as a blob, then downloads
17613 * it as a file. Only used in IE, in all other browsers we use the native
17614 * pdfMake.open function to just open the PDF
17615 * @param {string} fileName the filename to give to the pdf, can be set
17616 * through exporterPdfFilename
17617 * @param {object} docDefinition a pdf docDefinition that we can generate
17618 * and get a blob from
17619 */
17620 downloadPDF: function (fileName, docDefinition) {
17621 var D = document;
17622 var a = D.createElement('a');
17623 var strMimeType = 'application/octet-stream;charset=utf-8';
17624 var rawFile;
17625 var ieVersion;
17626
17627 ieVersion = this.isIE();
17628 var doc = pdfMake.createPdf(docDefinition);
17629 var blob;
17630
17631 doc.getBuffer( function (buffer) {
17632 blob = new Blob([buffer]);
17633
17634 if (ieVersion && ieVersion < 10) {
17635 var frame = D.createElement('iframe');
17636 document.body.appendChild(frame);
17637
17638 frame.contentWindow.document.open("text/html", "replace");
17639 frame.contentWindow.document.write(blob);
17640 frame.contentWindow.document.close();
17641 frame.contentWindow.focus();
17642 frame.contentWindow.document.execCommand('SaveAs', true, fileName);
17643
17644 document.body.removeChild(frame);
17645 return true;
17646 }
17647
17648 // IE10+
17649 if (navigator.msSaveBlob) {
17650 return navigator.msSaveBlob(
17651 blob, fileName
17652 );
17653 }
17654 });
17655 },
17656
17657
17658 /**
17659 * @ngdoc function
17660 * @name renderAsPdf
17661 * @methodOf ui.grid.exporter.service:uiGridExporterService
17662 * @description Renders the data into a pdf, and opens that pdf.
17663 *
17664 * @param {Grid} grid the grid from which data should be exported
17665 * @param {array} exportColumnHeaders an array of column headers,
17666 * where each header is an object with name, width and maybe alignment
17667 * @param {array} exportData an array of rows, where each row is
17668 * an array of column data
17669 * @returns {object} a pdfMake format document definition, ready
17670 * for generation
17671 */
17672 prepareAsPdf: function(grid, exportColumnHeaders, exportData) {
17673 var headerWidths = this.calculatePdfHeaderWidths( grid, exportColumnHeaders );
17674
17675 var headerColumns = exportColumnHeaders.map( function( header ) {
17676 return { text: header.displayName, style: 'tableHeader' };
17677 });
17678
17679 var stringData = exportData.map(this.formatRowAsPdf(this));
17680
17681 var allData = [headerColumns].concat(stringData);
17682
17683 var docDefinition = {
17684 pageOrientation: grid.options.exporterPdfOrientation,
17685 pageSize: grid.options.exporterPdfPageSize,
17686 content: [{
17687 style: 'tableStyle',
17688 table: {
17689 headerRows: 1,
17690 widths: headerWidths,
17691 body: allData
17692 }
17693 }],
17694 styles: {
17695 tableStyle: grid.options.exporterPdfTableStyle,
17696 tableHeader: grid.options.exporterPdfTableHeaderStyle
17697 },
17698 defaultStyle: grid.options.exporterPdfDefaultStyle
17699 };
17700
17701 if ( grid.options.exporterPdfLayout ){
17702 docDefinition.layout = grid.options.exporterPdfLayout;
17703 }
17704
17705 if ( grid.options.exporterPdfHeader ){
17706 docDefinition.header = grid.options.exporterPdfHeader;
17707 }
17708
17709 if ( grid.options.exporterPdfFooter ){
17710 docDefinition.footer = grid.options.exporterPdfFooter;
17711 }
17712
17713 if ( grid.options.exporterPdfCustomFormatter ){
17714 docDefinition = grid.options.exporterPdfCustomFormatter( docDefinition );
17715 }
17716 return docDefinition;
17717
17718 },
17719
17720
17721 /**
17722 * @ngdoc function
17723 * @name calculatePdfHeaderWidths
17724 * @methodOf ui.grid.exporter.service:uiGridExporterService
17725 * @description Determines the column widths base on the
17726 * widths we got from the grid. If the column is drawn
17727 * then we have a drawnWidth. If the column is not visible
17728 * then we have '*', 'x%' or a width. When columns are
17729 * not visible they don't contribute to the overall gridWidth,
17730 * so we need to adjust to allow for extra columns
17731 *
17732 * Our basic heuristic is to take the current gridWidth, plus
17733 * numeric columns and call this the base gridwidth.
17734 *
17735 * To that we add 100 for any '*' column, and x% of the base gridWidth
17736 * for any column that is a %
17737 *
17738 * @param {Grid} grid the grid from which data should be exported
17739 * @param {array} exportHeaders array of header information
17740 * @returns {object} an array of header widths
17741 */
17742 calculatePdfHeaderWidths: function ( grid, exportHeaders ) {
17743 var baseGridWidth = 0;
17744 exportHeaders.forEach( function(value){
17745 if (typeof(value.width) === 'number'){
17746 baseGridWidth += value.width;
17747 }
17748 });
17749
17750 var extraColumns = 0;
17751 exportHeaders.forEach( function(value){
17752 if (value.width === '*'){
17753 extraColumns += 100;
17754 }
17755 if (typeof(value.width) === 'string' && value.width.match(/(\d)*%/)) {
17756 var percent = parseInt(value.width.match(/(\d)*%/)[0]);
17757
17758 value.width = baseGridWidth * percent / 100;
17759 extraColumns += value.width;
17760 }
17761 });
17762
17763 var gridWidth = baseGridWidth + extraColumns;
17764
17765 return exportHeaders.map(function( header ) {
17766 return header.width === '*' ? header.width : header.width * grid.options.exporterPdfMaxGridWidth / gridWidth;
17767 });
17768
17769 },
17770
17771 /**
17772 * @ngdoc function
17773 * @name formatRowAsPdf
17774 * @methodOf ui.grid.exporter.service:uiGridExporterService
17775 * @description Renders a row in a format consumable by PDF,
17776 * mainly meaning casting everything to a string
17777 * @param {exporterService} exporter pass in exporter
17778 * @param {array} row the row to be turned into a csv string
17779 * @returns {string} a csv-ified version of the row
17780 */
17781 formatRowAsPdf: function ( exporter ) {
17782 return function( row ) {
17783 return row.map(exporter.formatFieldAsPdfString);
17784 };
17785 },
17786
17787
17788 /**
17789 * @ngdoc function
17790 * @name formatFieldAsCsv
17791 * @methodOf ui.grid.exporter.service:uiGridExporterService
17792 * @description Renders a single field as a pdf-able field, which
17793 * is different from a csv field only in that strings don't have quotes
17794 * around them
17795 * @param {field} field the field to be turned into a pdf string,
17796 * may be of any type
17797 * @returns {string} a string-ified version of the field
17798 */
17799 formatFieldAsPdfString: function (field) {
17800 var returnVal;
17801 if (field.value == null) { // we want to catch anything null-ish, hence just == not ===
17802 returnVal = '';
17803 } else if (typeof(field.value) === 'number') {
17804 returnVal = field.value.toString();
17805 } else if (typeof(field.value) === 'boolean') {
17806 returnVal = (field.value ? 'TRUE' : 'FALSE') ;
17807 } else if (typeof(field.value) === 'string') {
17808 returnVal = field.value.replace(/"/g,'""');
17809 } else {
17810 returnVal = JSON.stringify(field.value).replace(/^"/,'').replace(/"$/,'');
17811 }
17812
17813 if (field.alignment && typeof(field.alignment) === 'string' ){
17814 returnVal = { text: returnVal, alignment: field.alignment };
17815 }
17816
17817 return returnVal;
17818 }
17819 };
17820
17821 return service;
17822
17823 }
17824 ]);
17825
17826 /**
17827 * @ngdoc directive
17828 * @name ui.grid.exporter.directive:uiGridExporter
17829 * @element div
17830 * @restrict A
17831 *
17832 * @description Adds exporter features to grid
17833 *
17834 * @example
17835 <example module="app">
17836 <file name="app.js">
17837 var app = angular.module('app', ['ui.grid', 'ui.grid.exporter']);
17838
17839 app.controller('MainCtrl', ['$scope', function ($scope) {
17840 $scope.data = [
17841 { name: 'Bob', title: 'CEO' },
17842 { name: 'Frank', title: 'Lowly Developer' }
17843 ];
17844
17845 $scope.gridOptions = {
17846 enableGridMenu: true,
17847 exporterMenuCsv: false,
17848 columnDefs: [
17849 {name: 'name', enableCellEdit: true},
17850 {name: 'title', enableCellEdit: true}
17851 ],
17852 data: $scope.data
17853 };
17854 }]);
17855 </file>
17856 <file name="index.html">
17857 <div ng-controller="MainCtrl">
17858 <div ui-grid="gridOptions" ui-grid-exporter></div>
17859 </div>
17860 </file>
17861 </example>
17862 */
17863 module.directive('uiGridExporter', ['uiGridExporterConstants', 'uiGridExporterService', 'gridUtil', '$compile',
17864 function (uiGridExporterConstants, uiGridExporterService, gridUtil, $compile) {
17865 return {
17866 replace: true,
17867 priority: 0,
17868 require: '^uiGrid',
17869 scope: false,
17870 link: function ($scope, $elm, $attrs, uiGridCtrl) {
17871 uiGridExporterService.initializeGrid(uiGridCtrl.grid);
17872 uiGridCtrl.grid.exporter.$scope = $scope;
17873 }
17874 };
17875 }
17876 ]);
17877 })();
17878
17879 (function () {
17880 'use strict';
17881
17882 /**
17883 * @ngdoc overview
17884 * @name ui.grid.grouping
17885 * @description
17886 *
17887 * # ui.grid.grouping
17888 *
17889 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
17890 *
17891 * This module provides grouping of rows based on the data in them, similar
17892 * in concept to excel grouping. You can group multiple columns, resulting in
17893 * nested grouping.
17894 *
17895 * In concept this feature is similar to sorting + grid footer/aggregation, it
17896 * sorts the data based on the grouped columns, then creates group rows that
17897 * reflect a break in the data. Each of those group rows can have aggregations for
17898 * the data within that group.
17899 *
17900 * This feature leverages treeBase to provide the tree functionality itself,
17901 * the key thing this feature does therefore is to set treeLevels on the rows
17902 * and insert the group headers.
17903 *
17904 * Design information:
17905 * -------------------
17906 *
17907 * Each column will get new menu items - group by, and aggregate by. Group by
17908 * will cause this column to be sorted (if not already), and will move this column
17909 * to the front of the sorted columns (i.e. grouped columns take precedence over
17910 * sorted columns). It will respect the sort order already set if there is one,
17911 * and it will allow the sorting logic to change that sort order, it just forces
17912 * the column to the front of the sorting. You can group by multiple columns, the
17913 * logic will add this column to the sorting after any already grouped columns.
17914 *
17915 * Once a grouping is defined, grouping logic is added to the rowsProcessors. This
17916 * will process the rows, identifying a break in the data value, and inserting a grouping row.
17917 * Grouping rows have specific attributes on them:
17918 *
17919 * - internalRow = true: tells us that this isn't a real row, so we can ignore it
17920 * from any processing that it looking at core data rows. This is used by the core
17921 * logic (or will be one day), as it's not grouping specific
17922 * - groupHeader = true: tells us this is a groupHeader. This is used by the grouping logic
17923 * to know if this is a groupHeader row or not
17924 *
17925 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
17926 * row order or filtering or anything like that is changed. In order to avoid the row instantiation
17927 * time, and to preserve state across invocations, we hold a cache of the rows that we created
17928 * last time, and we use them again this time if we can.
17929 *
17930 * By default rows are collapsed, which means all data rows have their visible property
17931 * set to false, and only level 0 group rows are set to visible.
17932 *
17933 * <br/>
17934 * <br/>
17935 *
17936 * <div doc-module-components="ui.grid.grouping"></div>
17937 */
17938
17939 var module = angular.module('ui.grid.grouping', ['ui.grid', 'ui.grid.treeBase']);
17940
17941 /**
17942 * @ngdoc object
17943 * @name ui.grid.grouping.constant:uiGridGroupingConstants
17944 *
17945 * @description constants available in grouping module, this includes
17946 * all the constants declared in the treeBase module (these are manually copied
17947 * as there isn't an easy way to include constants in another constants file, and
17948 * we don't want to make users include treeBase)
17949 *
17950 */
17951 module.constant('uiGridGroupingConstants', {
17952 featureName: "grouping",
17953 rowHeaderColName: 'treeBaseRowHeaderCol',
17954 EXPANDED: 'expanded',
17955 COLLAPSED: 'collapsed',
17956 aggregation: {
17957 COUNT: 'count',
17958 SUM: 'sum',
17959 MAX: 'max',
17960 MIN: 'min',
17961 AVG: 'avg'
17962 }
17963 });
17964
17965 /**
17966 * @ngdoc service
17967 * @name ui.grid.grouping.service:uiGridGroupingService
17968 *
17969 * @description Services for grouping features
17970 */
17971 module.service('uiGridGroupingService', ['$q', 'uiGridGroupingConstants', 'gridUtil', 'rowSorter', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'uiGridTreeBaseService',
17972 function ($q, uiGridGroupingConstants, gridUtil, rowSorter, GridRow, gridClassFactory, i18nService, uiGridConstants, uiGridTreeBaseService) {
17973
17974 var service = {
17975
17976 initializeGrid: function (grid, $scope) {
17977 uiGridTreeBaseService.initializeGrid( grid, $scope );
17978
17979 //add feature namespace and any properties to grid for needed
17980 /**
17981 * @ngdoc object
17982 * @name ui.grid.grouping.grid:grouping
17983 *
17984 * @description Grid properties and functions added for grouping
17985 */
17986 grid.grouping = {};
17987
17988 /**
17989 * @ngdoc property
17990 * @propertyOf ui.grid.grouping.grid:grouping
17991 * @name groupHeaderCache
17992 *
17993 * @description Cache that holds the group header rows we created last time, we'll
17994 * reuse these next time, not least because they hold our expanded states.
17995 *
17996 * We need to take care with these that they don't become a memory leak, we
17997 * create a new cache each time using the values from the old cache. This works
17998 * so long as we're creating group rows for invisible rows as well.
17999 *
18000 * The cache is a nested hash, indexed on the value we grouped by. So if we
18001 * grouped by gender then age, we'd maybe have something like:
18002 * ```
18003 * {
18004 * male: {
18005 * row: <pointer to the old row>,
18006 * children: {
18007 * 22: { row: <pointer to the old row> },
18008 * 31: { row: <pointer to the old row> }
18009 * },
18010 * female: {
18011 * row: <pointer to the old row>,
18012 * children: {
18013 * 28: { row: <pointer to the old row> },
18014 * 55: { row: <pointer to the old row> }
18015 * }
18016 * }
18017 * ```
18018 *
18019 * We create new rows for any missing rows, this means that they come in as collapsed.
18020 *
18021 */
18022 grid.grouping.groupHeaderCache = {};
18023
18024 service.defaultGridOptions(grid.options);
18025
18026 grid.registerRowsProcessor(service.groupRows, 400);
18027
18028 grid.registerColumnBuilder( service.groupingColumnBuilder);
18029
18030 grid.registerColumnsProcessor(service.groupingColumnProcessor, 400);
18031
18032 /**
18033 * @ngdoc object
18034 * @name ui.grid.grouping.api:PublicApi
18035 *
18036 * @description Public Api for grouping feature
18037 */
18038 var publicApi = {
18039 events: {
18040 grouping: {
18041 /**
18042 * @ngdoc event
18043 * @eventOf ui.grid.grouping.api:PublicApi
18044 * @name aggregationChanged
18045 * @description raised whenever aggregation is changed, added or removed from a column
18046 *
18047 * <pre>
18048 * gridApi.grouping.on.aggregationChanged(scope,function(col){})
18049 * </pre>
18050 * @param {gridCol} col the column which on which aggregation changed. The aggregation
18051 * type is available as `col.treeAggregation.type`
18052 */
18053 aggregationChanged: {},
18054 /**
18055 * @ngdoc event
18056 * @eventOf ui.grid.grouping.api:PublicApi
18057 * @name groupingChanged
18058 * @description raised whenever the grouped columns changes
18059 *
18060 * <pre>
18061 * gridApi.grouping.on.groupingChanged(scope,function(col){})
18062 * </pre>
18063 * @param {gridCol} col the column which on which grouping changed. The new grouping is
18064 * available as `col.grouping`
18065 */
18066 groupingChanged: {}
18067 }
18068 },
18069 methods: {
18070 grouping: {
18071 /**
18072 * @ngdoc function
18073 * @name getGrouping
18074 * @methodOf ui.grid.grouping.api:PublicApi
18075 * @description Get the grouping configuration for this grid,
18076 * used by the saveState feature. Adds expandedState to the information
18077 * provided by the internal getGrouping, and removes any aggregations that have a source
18078 * of grouping (i.e. will be automatically reapplied when we regroup the column)
18079 * Returned grouping is an object
18080 * `{ grouping: groupArray, treeAggregations: aggregateArray, expandedState: hash }`
18081 * where grouping contains an array of objects:
18082 * `{ field: column.field, colName: column.name, groupPriority: column.grouping.groupPriority }`
18083 * and aggregations contains an array of objects:
18084 * `{ field: column.field, colName: column.name, aggregation: column.grouping.aggregation }`
18085 * and expandedState is a hash of the currently expanded nodes
18086 *
18087 * The groupArray will be sorted by groupPriority.
18088 *
18089 * @param {boolean} getExpanded whether or not to return the expanded state
18090 * @returns {object} grouping configuration
18091 */
18092 getGrouping: function ( getExpanded ) {
18093 var grouping = service.getGrouping(grid);
18094
18095 grouping.grouping.forEach( function( group ) {
18096 group.colName = group.col.name;
18097 delete group.col;
18098 });
18099
18100 grouping.aggregations.forEach( function( aggregation ) {
18101 aggregation.colName = aggregation.col.name;
18102 delete aggregation.col;
18103 });
18104
18105 grouping.aggregations = grouping.aggregations.filter( function( aggregation ){
18106 return !aggregation.aggregation.source || aggregation.aggregation.source !== 'grouping';
18107 });
18108
18109 if ( getExpanded ){
18110 grouping.rowExpandedStates = service.getRowExpandedStates( grid.grouping.groupingHeaderCache );
18111 }
18112
18113 return grouping;
18114 },
18115
18116 /**
18117 * @ngdoc function
18118 * @name setGrouping
18119 * @methodOf ui.grid.grouping.api:PublicApi
18120 * @description Set the grouping configuration for this grid,
18121 * used by the saveState feature, but can also be used by any
18122 * user to specify a combined grouping and aggregation configuration
18123 * @param {object} config the config you want to apply, in the format
18124 * provided out by getGrouping
18125 */
18126 setGrouping: function ( config ) {
18127 service.setGrouping(grid, config);
18128 },
18129
18130 /**
18131 * @ngdoc function
18132 * @name groupColumn
18133 * @methodOf ui.grid.grouping.api:PublicApi
18134 * @description Adds this column to the existing grouping, at the end of the priority order.
18135 * If the column doesn't have a sort, adds one, by default ASC
18136 *
18137 * This column will move to the left of any non-group columns, the
18138 * move is handled in a columnProcessor, so gets called as part of refresh
18139 *
18140 * @param {string} columnName the name of the column we want to group
18141 */
18142 groupColumn: function( columnName ) {
18143 var column = grid.getColumn(columnName);
18144 service.groupColumn(grid, column);
18145 },
18146
18147 /**
18148 * @ngdoc function
18149 * @name ungroupColumn
18150 * @methodOf ui.grid.grouping.api:PublicApi
18151 * @description Removes the groupPriority from this column. If the
18152 * column was previously aggregated the aggregation will come back.
18153 * The sort will remain.
18154 *
18155 * This column will move to the right of any other group columns, the
18156 * move is handled in a columnProcessor, so gets called as part of refresh
18157 *
18158 * @param {string} columnName the name of the column we want to ungroup
18159 */
18160 ungroupColumn: function( columnName ) {
18161 var column = grid.getColumn(columnName);
18162 service.ungroupColumn(grid, column);
18163 },
18164
18165 /**
18166 * @ngdoc function
18167 * @name clearGrouping
18168 * @methodOf ui.grid.grouping.api:PublicApi
18169 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
18170 * as we don't know whether that sorting was added by grouping or was there beforehand
18171 *
18172 */
18173 clearGrouping: function() {
18174 service.clearGrouping(grid);
18175 },
18176
18177 /**
18178 * @ngdoc function
18179 * @name aggregateColumn
18180 * @methodOf ui.grid.grouping.api:PublicApi
18181 * @description Sets the aggregation type on a column, if the
18182 * column is currently grouped then it removes the grouping first.
18183 * If the aggregationDef is null then will result in the aggregation
18184 * being removed
18185 *
18186 * @param {string} columnName the column we want to aggregate
18187 * @param {string} or {function} aggregationDef one of the recognised types
18188 * from uiGridGroupingConstants or a custom aggregation function.
18189 * @param {string} aggregationLabel (optional) The label to use for this aggregation.
18190 */
18191 aggregateColumn: function( columnName, aggregationDef, aggregationLabel){
18192 var column = grid.getColumn(columnName);
18193 service.aggregateColumn( grid, column, aggregationDef, aggregationLabel);
18194 }
18195
18196 }
18197 }
18198 };
18199
18200 grid.api.registerEventsFromObject(publicApi.events);
18201
18202 grid.api.registerMethodsFromObject(publicApi.methods);
18203
18204 grid.api.core.on.sortChanged( $scope, service.tidyPriorities);
18205
18206 },
18207
18208 defaultGridOptions: function (gridOptions) {
18209 //default option to true unless it was explicitly set to false
18210 /**
18211 * @ngdoc object
18212 * @name ui.grid.grouping.api:GridOptions
18213 *
18214 * @description GridOptions for grouping feature, these are available to be
18215 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
18216 */
18217
18218 /**
18219 * @ngdoc object
18220 * @name enableGrouping
18221 * @propertyOf ui.grid.grouping.api:GridOptions
18222 * @description Enable row grouping for entire grid.
18223 * <br/>Defaults to true
18224 */
18225 gridOptions.enableGrouping = gridOptions.enableGrouping !== false;
18226
18227 /**
18228 * @ngdoc object
18229 * @name groupingShowCounts
18230 * @propertyOf ui.grid.grouping.api:GridOptions
18231 * @description shows counts on the groupHeader rows. Not that if you are using a cellFilter or a
18232 * sortingAlgorithm which relies on a specific format or data type, showing counts may cause that
18233 * to break, since the group header rows will always be a string with groupingShowCounts enabled.
18234 * <br/>Defaults to true except on columns of type 'date'
18235 */
18236 gridOptions.groupingShowCounts = gridOptions.groupingShowCounts !== false;
18237
18238 /**
18239 * @ngdoc object
18240 * @name groupingNullLabel
18241 * @propertyOf ui.grid.grouping.api:GridOptions
18242 * @description The string to use for the grouping header row label on rows which contain a null or undefined value in the grouped column.
18243 * <br/>Defaults to "Null"
18244 */
18245 gridOptions.groupingNullLabel = typeof(gridOptions.groupingNullLabel) === 'undefined' ? 'Null' : gridOptions.groupingNullLabel;
18246
18247 /**
18248 * @ngdoc object
18249 * @name enableGroupHeaderSelection
18250 * @propertyOf ui.grid.grouping.api:GridOptions
18251 * @description Allows group header rows to be selected.
18252 * <br/>Defaults to false
18253 */
18254 gridOptions.enableGroupHeaderSelection = gridOptions.enableGroupHeaderSelection === true;
18255 },
18256
18257
18258 /**
18259 * @ngdoc function
18260 * @name groupingColumnBuilder
18261 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18262 * @description Sets the grouping defaults based on the columnDefs
18263 *
18264 * @param {object} colDef columnDef we're basing on
18265 * @param {GridCol} col the column we're to update
18266 * @param {object} gridOptions the options we should use
18267 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
18268 */
18269 groupingColumnBuilder: function (colDef, col, gridOptions) {
18270 /**
18271 * @ngdoc object
18272 * @name ui.grid.grouping.api:ColumnDef
18273 *
18274 * @description ColumnDef for grouping feature, these are available to be
18275 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
18276 */
18277
18278 /**
18279 * @ngdoc object
18280 * @name enableGrouping
18281 * @propertyOf ui.grid.grouping.api:ColumnDef
18282 * @description Enable grouping on this column
18283 * <br/>Defaults to true.
18284 */
18285 if (colDef.enableGrouping === false){
18286 return;
18287 }
18288
18289 /**
18290 * @ngdoc object
18291 * @name grouping
18292 * @propertyOf ui.grid.grouping.api:ColumnDef
18293 * @description Set the grouping for a column. Format is:
18294 * ```
18295 * {
18296 * groupPriority: <number, starts at 0, if less than 0 or undefined then we're aggregating in this column>
18297 * }
18298 * ```
18299 *
18300 * **Note that aggregation used to be included in grouping, but is now separately set on the column via treeAggregation
18301 * setting in treeBase**
18302 *
18303 * We group in the priority order given, this will also put these columns to the high order of the sort irrespective
18304 * of the sort priority given them. If there is no sort defined then we sort ascending, if there is a sort defined then
18305 * we use that sort.
18306 *
18307 * If the groupPriority is undefined or less than 0, then we expect to be aggregating, and we look at the
18308 * aggregation types to determine what sort of aggregation we can do. Values are in the constants file, but
18309 * include SUM, COUNT, MAX, MIN
18310 *
18311 * groupPriorities should generally be sequential, if they're not then the next time getGrouping is called
18312 * we'll renumber them to be sequential.
18313 * <br/>Defaults to undefined.
18314 */
18315
18316 if ( typeof(col.grouping) === 'undefined' && typeof(colDef.grouping) !== 'undefined') {
18317 col.grouping = angular.copy(colDef.grouping);
18318 if ( typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority > -1 ){
18319 col.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
18320 col.treeAggregationFinalizerFn = service.groupedFinalizerFn;
18321 }
18322 } else if (typeof(col.grouping) === 'undefined'){
18323 col.grouping = {};
18324 }
18325
18326 if (typeof(col.grouping) !== 'undefined' && typeof(col.grouping.groupPriority) !== 'undefined' && col.grouping.groupPriority >= 0){
18327 col.suppressRemoveSort = true;
18328 }
18329
18330 var groupColumn = {
18331 name: 'ui.grid.grouping.group',
18332 title: i18nService.get().grouping.group,
18333 icon: 'ui-grid-icon-indent-right',
18334 shown: function () {
18335 return typeof(this.context.col.grouping) === 'undefined' ||
18336 typeof(this.context.col.grouping.groupPriority) === 'undefined' ||
18337 this.context.col.grouping.groupPriority < 0;
18338 },
18339 action: function () {
18340 service.groupColumn( this.context.col.grid, this.context.col );
18341 }
18342 };
18343
18344 var ungroupColumn = {
18345 name: 'ui.grid.grouping.ungroup',
18346 title: i18nService.get().grouping.ungroup,
18347 icon: 'ui-grid-icon-indent-left',
18348 shown: function () {
18349 return typeof(this.context.col.grouping) !== 'undefined' &&
18350 typeof(this.context.col.grouping.groupPriority) !== 'undefined' &&
18351 this.context.col.grouping.groupPriority >= 0;
18352 },
18353 action: function () {
18354 service.ungroupColumn( this.context.col.grid, this.context.col );
18355 }
18356 };
18357
18358 var aggregateRemove = {
18359 name: 'ui.grid.grouping.aggregateRemove',
18360 title: i18nService.get().grouping.aggregate_remove,
18361 shown: function () {
18362 return typeof(this.context.col.treeAggregationFn) !== 'undefined';
18363 },
18364 action: function () {
18365 service.aggregateColumn( this.context.col.grid, this.context.col, null);
18366 }
18367 };
18368
18369 // generic adder for the aggregation menus, which follow a pattern
18370 var addAggregationMenu = function(type, title){
18371 title = title || i18nService.get().grouping['aggregate_' + type] || type;
18372 var menuItem = {
18373 name: 'ui.grid.grouping.aggregate' + type,
18374 title: title,
18375 shown: function () {
18376 return typeof(this.context.col.treeAggregation) === 'undefined' ||
18377 typeof(this.context.col.treeAggregation.type) === 'undefined' ||
18378 this.context.col.treeAggregation.type !== type;
18379 },
18380 action: function () {
18381 service.aggregateColumn( this.context.col.grid, this.context.col, type);
18382 }
18383 };
18384
18385 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregate' + type)) {
18386 col.menuItems.push(menuItem);
18387 }
18388 };
18389
18390 /**
18391 * @ngdoc object
18392 * @name groupingShowGroupingMenu
18393 * @propertyOf ui.grid.grouping.api:ColumnDef
18394 * @description Show the grouping (group and ungroup items) menu on this column
18395 * <br/>Defaults to true.
18396 */
18397 if ( col.colDef.groupingShowGroupingMenu !== false ){
18398 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.group')) {
18399 col.menuItems.push(groupColumn);
18400 }
18401
18402 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.ungroup')) {
18403 col.menuItems.push(ungroupColumn);
18404 }
18405 }
18406
18407
18408 /**
18409 * @ngdoc object
18410 * @name groupingShowAggregationMenu
18411 * @propertyOf ui.grid.grouping.api:ColumnDef
18412 * @description Show the aggregation menu on this column
18413 * <br/>Defaults to true.
18414 */
18415 if ( col.colDef.groupingShowAggregationMenu !== false ){
18416 angular.forEach(uiGridTreeBaseService.nativeAggregations(), function(aggregationDef, name){
18417 addAggregationMenu(name);
18418 });
18419 angular.forEach(gridOptions.treeCustomAggregations, function(aggregationDef, name){
18420 addAggregationMenu(name, aggregationDef.menuTitle);
18421 });
18422
18423 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.grouping.aggregateRemove')) {
18424 col.menuItems.push(aggregateRemove);
18425 }
18426 }
18427 },
18428
18429
18430
18431
18432 /**
18433 * @ngdoc function
18434 * @name groupingColumnProcessor
18435 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18436 * @description Moves the columns around based on which are grouped
18437 *
18438 * @param {array} columns the columns to consider rendering
18439 * @param {array} rows the grid rows, which we don't use but are passed to us
18440 * @returns {array} updated columns array
18441 */
18442 groupingColumnProcessor: function( columns, rows ) {
18443 var grid = this;
18444
18445 columns = service.moveGroupColumns(this, columns, rows);
18446 return columns;
18447 },
18448
18449 /**
18450 * @ngdoc function
18451 * @name groupedFinalizerFn
18452 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18453 * @description Used on group columns to display the rendered value and optionally
18454 * display the count of rows.
18455 *
18456 * @param {aggregation} the aggregation entity for a grouped column
18457 */
18458 groupedFinalizerFn: function( aggregation ){
18459 var col = this;
18460
18461 if ( typeof(aggregation.groupVal) !== 'undefined') {
18462 aggregation.rendered = aggregation.groupVal;
18463 if ( col.grid.options.groupingShowCounts && col.colDef.type !== 'date' ){
18464 aggregation.rendered += (' (' + aggregation.value + ')');
18465 }
18466 } else {
18467 aggregation.rendered = null;
18468 }
18469 },
18470
18471 /**
18472 * @ngdoc function
18473 * @name moveGroupColumns
18474 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18475 * @description Moves the column order so that the grouped columns are lined up
18476 * to the left (well, unless you're RTL, then it's the right). By doing this in
18477 * the columnsProcessor, we make it transient - when the column is ungrouped it'll
18478 * go back to where it was.
18479 *
18480 * Does nothing if the option `moveGroupColumns` is set to false.
18481 *
18482 * @param {Grid} grid grid object
18483 * @param {array} columns the columns that we should process/move
18484 * @param {array} rows the grid rows
18485 * @returns {array} updated columns
18486 */
18487 moveGroupColumns: function( grid, columns, rows ){
18488 if ( grid.options.moveGroupColumns === false){
18489 return columns;
18490 }
18491
18492 columns.forEach( function(column, index){
18493 // position used to make stable sort in moveGroupColumns
18494 column.groupingPosition = index;
18495 });
18496
18497 columns.sort(function(a, b){
18498 var a_group, b_group;
18499 if (a.isRowHeader){
18500 a_group = -1000;
18501 }
18502 else if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){
18503 a_group = null;
18504 } else {
18505 a_group = a.grouping.groupPriority;
18506 }
18507
18508 if (b.isRowHeader){
18509 b_group = -1000;
18510 }
18511 else if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){
18512 b_group = null;
18513 } else {
18514 b_group = b.grouping.groupPriority;
18515 }
18516
18517 // groups get sorted to the top
18518 if ( a_group !== null && b_group === null) { return -1; }
18519 if ( b_group !== null && a_group === null) { return 1; }
18520 if ( a_group !== null && b_group !== null) {return a_group - b_group; }
18521
18522 return a.groupingPosition - b.groupingPosition;
18523 });
18524
18525 columns.forEach( function(column, index) {
18526 delete column.groupingPosition;
18527 });
18528
18529 return columns;
18530 },
18531
18532
18533 /**
18534 * @ngdoc function
18535 * @name groupColumn
18536 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18537 * @description Adds this column to the existing grouping, at the end of the priority order.
18538 * If the column doesn't have a sort, adds one, by default ASC
18539 *
18540 * This column will move to the left of any non-group columns, the
18541 * move is handled in a columnProcessor, so gets called as part of refresh
18542 *
18543 * @param {Grid} grid grid object
18544 * @param {GridCol} column the column we want to group
18545 */
18546 groupColumn: function( grid, column){
18547 if ( typeof(column.grouping) === 'undefined' ){
18548 column.grouping = {};
18549 }
18550
18551 // set the group priority to the next number in the hierarchy
18552 var existingGrouping = service.getGrouping( grid );
18553 column.grouping.groupPriority = existingGrouping.grouping.length;
18554
18555 // add sort if not present
18556 if ( !column.sort ){
18557 column.sort = { direction: uiGridConstants.ASC };
18558 } else if ( typeof(column.sort.direction) === 'undefined' || column.sort.direction === null ){
18559 column.sort.direction = uiGridConstants.ASC;
18560 }
18561
18562 column.treeAggregation = { type: uiGridGroupingConstants.aggregation.COUNT, source: 'grouping' };
18563 column.treeAggregationFn = uiGridTreeBaseService.nativeAggregations()[uiGridGroupingConstants.aggregation.COUNT].aggregationFn;
18564 column.treeAggregationFinalizerFn = service.groupedFinalizerFn;
18565
18566 grid.api.grouping.raise.groupingChanged(column);
18567 // This indirectly calls service.tidyPriorities( grid );
18568 grid.api.core.raise.sortChanged(grid, grid.getColumnSorting());
18569
18570 grid.queueGridRefresh();
18571 },
18572
18573
18574 /**
18575 * @ngdoc function
18576 * @name ungroupColumn
18577 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18578 * @description Removes the groupPriority from this column. If the
18579 * column was previously aggregated the aggregation will come back.
18580 * The sort will remain.
18581 *
18582 * This column will move to the right of any other group columns, the
18583 * move is handled in a columnProcessor, so gets called as part of refresh
18584 *
18585 * @param {Grid} grid grid object
18586 * @param {GridCol} column the column we want to ungroup
18587 */
18588 ungroupColumn: function( grid, column){
18589 if ( typeof(column.grouping) === 'undefined' ){
18590 return;
18591 }
18592
18593 delete column.grouping.groupPriority;
18594 delete column.treeAggregation;
18595 delete column.customTreeAggregationFinalizer;
18596
18597 service.tidyPriorities( grid );
18598
18599 grid.api.grouping.raise.groupingChanged(column);
18600
18601 grid.queueGridRefresh();
18602 },
18603
18604 /**
18605 * @ngdoc function
18606 * @name aggregateColumn
18607 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18608 * @description Sets the aggregation type on a column, if the
18609 * column is currently grouped then it removes the grouping first.
18610 *
18611 * @param {Grid} grid grid object
18612 * @param {GridCol} column the column we want to aggregate
18613 * @param {string} one of the recognised types from uiGridGroupingConstants or one of the custom aggregations from gridOptions
18614 */
18615 aggregateColumn: function( grid, column, aggregationType){
18616
18617 if (typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
18618 service.ungroupColumn( grid, column );
18619 }
18620
18621 var aggregationDef = {};
18622 if ( typeof(grid.options.treeCustomAggregations[aggregationType]) !== 'undefined' ){
18623 aggregationDef = grid.options.treeCustomAggregations[aggregationType];
18624 } else if ( typeof(uiGridTreeBaseService.nativeAggregations()[aggregationType]) !== 'undefined' ){
18625 aggregationDef = uiGridTreeBaseService.nativeAggregations()[aggregationType];
18626 }
18627
18628 column.treeAggregation = { type: aggregationType, label: i18nService.get().aggregation[aggregationDef.label] || aggregationDef.label };
18629 column.treeAggregationFn = aggregationDef.aggregationFn;
18630 column.treeAggregationFinalizerFn = aggregationDef.finalizerFn;
18631
18632 grid.api.grouping.raise.aggregationChanged(column);
18633
18634 grid.queueGridRefresh();
18635 },
18636
18637
18638 /**
18639 * @ngdoc function
18640 * @name setGrouping
18641 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18642 * @description Set the grouping based on a config object, used by the save state feature
18643 * (more specifically, by the restore function in that feature )
18644 *
18645 * @param {Grid} grid grid object
18646 * @param {object} config the config we want to set, same format as that returned by getGrouping
18647 */
18648 setGrouping: function ( grid, config ){
18649 if ( typeof(config) === 'undefined' ){
18650 return;
18651 }
18652
18653 // first remove any existing grouping
18654 service.clearGrouping(grid);
18655
18656 if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){
18657 config.grouping.forEach( function( group ) {
18658 var col = grid.getColumn(group.colName);
18659
18660 if ( col ) {
18661 service.groupColumn( grid, col );
18662 }
18663 });
18664 }
18665
18666 if ( config.aggregations && config.aggregations.length ){
18667 config.aggregations.forEach( function( aggregation ) {
18668 var col = grid.getColumn(aggregation.colName);
18669
18670 if ( col ) {
18671 service.aggregateColumn( grid, col, aggregation.aggregation.type );
18672 }
18673 });
18674 }
18675
18676 if ( config.rowExpandedStates ){
18677 service.applyRowExpandedStates( grid.grouping.groupingHeaderCache, config.rowExpandedStates );
18678 }
18679 },
18680
18681
18682 /**
18683 * @ngdoc function
18684 * @name clearGrouping
18685 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18686 * @description Clear any grouped columns and any aggregations. Doesn't remove sorting,
18687 * as we don't know whether that sorting was added by grouping or was there beforehand
18688 *
18689 * @param {Grid} grid grid object
18690 */
18691 clearGrouping: function( grid ) {
18692 var currentGrouping = service.getGrouping(grid);
18693
18694 if ( currentGrouping.grouping.length > 0 ){
18695 currentGrouping.grouping.forEach( function( group ) {
18696 if (!group.col){
18697 // should have a group.colName if there's no col
18698 group.col = grid.getColumn(group.colName);
18699 }
18700 service.ungroupColumn(grid, group.col);
18701 });
18702 }
18703
18704 if ( currentGrouping.aggregations.length > 0 ){
18705 currentGrouping.aggregations.forEach( function( aggregation ){
18706 if (!aggregation.col){
18707 // should have a group.colName if there's no col
18708 aggregation.col = grid.getColumn(aggregation.colName);
18709 }
18710 service.aggregateColumn(grid, aggregation.col, null);
18711 });
18712 }
18713 },
18714
18715
18716 /**
18717 * @ngdoc function
18718 * @name tidyPriorities
18719 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18720 * @description Renumbers groupPriority and sortPriority such that
18721 * groupPriority is contiguous, and sortPriority either matches
18722 * groupPriority (for group columns), and otherwise is contiguous and
18723 * higher than groupPriority.
18724 *
18725 * @param {Grid} grid grid object
18726 */
18727 tidyPriorities: function( grid ){
18728 // if we're called from sortChanged, grid is in this, not passed as param, the param can be a column or undefined
18729 if ( ( typeof(grid) === 'undefined' || typeof(grid.grid) !== 'undefined' ) && typeof(this.grid) !== 'undefined' ) {
18730 grid = this.grid;
18731 }
18732
18733 var groupArray = [];
18734 var sortArray = [];
18735
18736 grid.columns.forEach( function(column, index){
18737 if ( typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
18738 groupArray.push(column);
18739 } else if ( typeof(column.sort) !== 'undefined' && typeof(column.sort.priority) !== 'undefined' && column.sort.priority >= 0){
18740 sortArray.push(column);
18741 }
18742 });
18743
18744 groupArray.sort(function(a, b){ return a.grouping.groupPriority - b.grouping.groupPriority; });
18745 groupArray.forEach( function(column, index){
18746 column.grouping.groupPriority = index;
18747 column.suppressRemoveSort = true;
18748 if ( typeof(column.sort) === 'undefined'){
18749 column.sort = {};
18750 }
18751 column.sort.priority = index;
18752 });
18753
18754 var i = groupArray.length;
18755 sortArray.sort(function(a, b){ return a.sort.priority - b.sort.priority; });
18756 sortArray.forEach( function(column, index){
18757 column.sort.priority = i;
18758 column.suppressRemoveSort = column.colDef.suppressRemoveSort;
18759 i++;
18760 });
18761 },
18762
18763
18764 /**
18765 * @ngdoc function
18766 * @name groupRows
18767 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18768 * @description The rowProcessor that creates the groupHeaders (i.e. does
18769 * the actual grouping).
18770 *
18771 * Assumes it is always called after the sorting processor, guaranteed by the priority setting
18772 *
18773 * Processes all the rows in order, inserting a groupHeader row whenever there is a change
18774 * in value of a grouped row, based on the sortAlgorithm used for the column. The group header row
18775 * is looked up in the groupHeaderCache, and used from there if there is one. The entity is reset
18776 * to {} if one is found.
18777 *
18778 * As it processes it maintains a `processingState` array. This records, for each level of grouping we're
18779 * working with, the following information:
18780 * ```
18781 * {
18782 * fieldName: name,
18783 * col: col,
18784 * initialised: boolean,
18785 * currentValue: value,
18786 * currentRow: gridRow,
18787 * }
18788 * ```
18789 * We look for changes in the currentValue at any of the levels. Where we find a change we:
18790 *
18791 * - create a new groupHeader row in the array
18792 *
18793 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
18794 * @returns {array} the updated rows, including our new group rows
18795 */
18796 groupRows: function( renderableRows ) {
18797 if (renderableRows.length === 0){
18798 return renderableRows;
18799 }
18800
18801 var grid = this;
18802 grid.grouping.oldGroupingHeaderCache = grid.grouping.groupingHeaderCache || {};
18803 grid.grouping.groupingHeaderCache = {};
18804
18805 var processingState = service.initialiseProcessingState( grid );
18806
18807 // processes each of the fields we are grouping by, checks if the value has changed and inserts a groupHeader
18808 // Broken out as shouldn't create functions in a loop.
18809 var updateProcessingState = function( groupFieldState, stateIndex ) {
18810 var fieldValue = grid.getCellValue(row, groupFieldState.col);
18811
18812 // look for change of value - and insert a header
18813 if ( !groupFieldState.initialised || rowSorter.getSortFn(grid, groupFieldState.col, renderableRows)(fieldValue, groupFieldState.currentValue) !== 0 ){
18814 service.insertGroupHeader( grid, renderableRows, i, processingState, stateIndex );
18815 i++;
18816 }
18817 };
18818
18819 // use a for loop because it's tolerant of the array length changing whilst we go - we can
18820 // manipulate the iterator when we insert groupHeader rows
18821 for (var i = 0; i < renderableRows.length; i++ ){
18822 var row = renderableRows[i];
18823
18824 if ( row.visible ){
18825 processingState.forEach( updateProcessingState );
18826 }
18827 }
18828
18829 delete grid.grouping.oldGroupingHeaderCache;
18830 return renderableRows;
18831 },
18832
18833
18834 /**
18835 * @ngdoc function
18836 * @name initialiseProcessingState
18837 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18838 * @description Creates the processing state array that is used
18839 * for groupRows.
18840 *
18841 * @param {Grid} grid grid object
18842 * @returns {array} an array in the format described in the groupRows method,
18843 * initialised with blank values
18844 */
18845 initialiseProcessingState: function( grid ){
18846 var processingState = [];
18847 var columnSettings = service.getGrouping( grid );
18848
18849 columnSettings.grouping.forEach( function( groupItem, index){
18850 processingState.push({
18851 fieldName: groupItem.field,
18852 col: groupItem.col,
18853 initialised: false,
18854 currentValue: null,
18855 currentRow: null
18856 });
18857 });
18858
18859 return processingState;
18860 },
18861
18862
18863 /**
18864 * @ngdoc function
18865 * @name getGrouping
18866 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18867 * @description Get the grouping settings from the columns. As a side effect
18868 * this always renumbers the grouping starting at 0
18869 * @param {Grid} grid grid object
18870 * @returns {array} an array of the group fields, in order of priority
18871 */
18872 getGrouping: function( grid ){
18873 var groupArray = [];
18874 var aggregateArray = [];
18875
18876 // get all the grouping
18877 grid.columns.forEach( function(column, columnIndex){
18878 if ( column.grouping ){
18879 if ( typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){
18880 groupArray.push({ field: column.field, col: column, groupPriority: column.grouping.groupPriority, grouping: column.grouping });
18881 }
18882 }
18883 if ( column.treeAggregation && column.treeAggregation.type ){
18884 aggregateArray.push({ field: column.field, col: column, aggregation: column.treeAggregation });
18885 }
18886 });
18887
18888 // sort grouping into priority order
18889 groupArray.sort( function(a, b){
18890 return a.groupPriority - b.groupPriority;
18891 });
18892
18893 // renumber the priority in case it was somewhat messed up, then remove the grouping reference
18894 groupArray.forEach( function( group, index) {
18895 group.grouping.groupPriority = index;
18896 group.groupPriority = index;
18897 delete group.grouping;
18898 });
18899
18900 return { grouping: groupArray, aggregations: aggregateArray };
18901 },
18902
18903
18904 /**
18905 * @ngdoc function
18906 * @name insertGroupHeader
18907 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18908 * @description Create a group header row, and link it to the various configuration
18909 * items that we use.
18910 *
18911 * Look for the row in the oldGroupingHeaderCache, write the row into the new groupingHeaderCache.
18912 *
18913 * @param {Grid} grid grid object
18914 * @param {array} renderableRows the rows that we are processing
18915 * @param {number} rowIndex the row we were up to processing
18916 * @param {array} processingState the current processing state
18917 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header -
18918 * i.e. the column that we want to create a header for
18919 */
18920 insertGroupHeader: function( grid, renderableRows, rowIndex, processingState, stateIndex ) {
18921 // set the value that caused the end of a group into the header row and the processing state
18922 var fieldName = processingState[stateIndex].fieldName;
18923 var col = processingState[stateIndex].col;
18924
18925 var newValue = grid.getCellValue(renderableRows[rowIndex], col);
18926 var newDisplayValue = newValue;
18927 if ( typeof(newValue) === 'undefined' || newValue === null ) {
18928 newDisplayValue = grid.options.groupingNullLabel;
18929 }
18930
18931 var cacheItem = grid.grouping.oldGroupingHeaderCache;
18932 for ( var i = 0; i < stateIndex; i++ ){
18933 if ( cacheItem && cacheItem[processingState[i].currentValue] ){
18934 cacheItem = cacheItem[processingState[i].currentValue].children;
18935 }
18936 }
18937
18938 var headerRow;
18939 if ( cacheItem && cacheItem[newValue]){
18940 headerRow = cacheItem[newValue].row;
18941 headerRow.entity = {};
18942 } else {
18943 headerRow = new GridRow( {}, null, grid );
18944 gridClassFactory.rowTemplateAssigner.call(grid, headerRow);
18945 }
18946
18947 headerRow.entity['$$' + processingState[stateIndex].col.uid] = { groupVal: newDisplayValue };
18948 headerRow.treeLevel = stateIndex;
18949 headerRow.groupHeader = true;
18950 headerRow.internalRow = true;
18951 headerRow.enableCellEdit = false;
18952 headerRow.enableSelection = grid.options.enableGroupHeaderSelection;
18953 processingState[stateIndex].initialised = true;
18954 processingState[stateIndex].currentValue = newValue;
18955 processingState[stateIndex].currentRow = headerRow;
18956
18957 // set all processing states below this one to not be initialised - change of this state
18958 // means all those need to start again
18959 service.finaliseProcessingState( processingState, stateIndex + 1);
18960
18961 // insert our new header row
18962 renderableRows.splice(rowIndex, 0, headerRow);
18963
18964 // add our new header row to the cache
18965 cacheItem = grid.grouping.groupingHeaderCache;
18966 for ( i = 0; i < stateIndex; i++ ){
18967 cacheItem = cacheItem[processingState[i].currentValue].children;
18968 }
18969 cacheItem[newValue] = { row: headerRow, children: {} };
18970 },
18971
18972
18973 /**
18974 * @ngdoc function
18975 * @name finaliseProcessingState
18976 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18977 * @description Set all processing states lower than the one that had a break in value to
18978 * no longer be initialised. Render the counts into the entity ready for display.
18979 *
18980 * @param {Grid} grid grid object
18981 * @param {array} processingState the current processing state
18982 * @param {number} stateIndex the processing state item that we were on when we triggered a new group header, all
18983 * processing states after this need to be finalised
18984 */
18985 finaliseProcessingState: function( processingState, stateIndex ){
18986 for ( var i = stateIndex; i < processingState.length; i++){
18987 processingState[i].initialised = false;
18988 processingState[i].currentRow = null;
18989 processingState[i].currentValue = null;
18990 }
18991 },
18992
18993
18994 /**
18995 * @ngdoc function
18996 * @name getRowExpandedStates
18997 * @methodOf ui.grid.grouping.service:uiGridGroupingService
18998 * @description Extract the groupHeaderCache hash, pulling out only the states.
18999 *
19000 * The example below shows a grid that is grouped by gender then age
19001 *
19002 * <pre>
19003 * {
19004 * male: {
19005 * state: 'expanded',
19006 * children: {
19007 * 22: { state: 'expanded' },
19008 * 30: { state: 'collapsed' }
19009 * }
19010 * },
19011 * female: {
19012 * state: 'expanded',
19013 * children: {
19014 * 28: { state: 'expanded' },
19015 * 55: { state: 'collapsed' }
19016 * }
19017 * }
19018 * }
19019 * </pre>
19020 *
19021 * @param {Grid} grid grid object
19022 * @returns {hash} the expanded states as a hash
19023 */
19024 getRowExpandedStates: function(treeChildren){
19025 if ( typeof(treeChildren) === 'undefined' ){
19026 return {};
19027 }
19028
19029 var newChildren = {};
19030
19031 angular.forEach( treeChildren, function( value, key ){
19032 newChildren[key] = { state: value.row.treeNode.state };
19033 if ( value.children ){
19034 newChildren[key].children = service.getRowExpandedStates( value.children );
19035 } else {
19036 newChildren[key].children = {};
19037 }
19038 });
19039
19040 return newChildren;
19041 },
19042
19043
19044 /**
19045 * @ngdoc function
19046 * @name applyRowExpandedStates
19047 * @methodOf ui.grid.grouping.service:uiGridGroupingService
19048 * @description Take a hash in the format as created by getRowExpandedStates,
19049 * and apply it to the grid.grouping.groupHeaderCache.
19050 *
19051 * Takes a treeSubset, and applies to a treeSubset - so can be called
19052 * recursively.
19053 *
19054 * @param {object} currentNode can be grid.grouping.groupHeaderCache, or any of
19055 * the children of that hash
19056 * @returns {hash} expandedStates can be the full expanded states, or children
19057 * of that expanded states (which hopefully matches the subset of the groupHeaderCache)
19058 */
19059 applyRowExpandedStates: function( currentNode, expandedStates ){
19060 if ( typeof(expandedStates) === 'undefined' ){
19061 return;
19062 }
19063
19064 angular.forEach(expandedStates, function( value, key ) {
19065 if ( currentNode[key] ){
19066 currentNode[key].row.treeNode.state = value.state;
19067
19068 if (value.children && currentNode[key].children){
19069 service.applyRowExpandedStates( currentNode[key].children, value.children );
19070 }
19071 }
19072 });
19073 }
19074
19075
19076 };
19077
19078 return service;
19079
19080 }]);
19081
19082
19083 /**
19084 * @ngdoc directive
19085 * @name ui.grid.grouping.directive:uiGridGrouping
19086 * @element div
19087 * @restrict A
19088 *
19089 * @description Adds grouping features to grid
19090 *
19091 * @example
19092 <example module="app">
19093 <file name="app.js">
19094 var app = angular.module('app', ['ui.grid', 'ui.grid.grouping']);
19095
19096 app.controller('MainCtrl', ['$scope', function ($scope) {
19097 $scope.data = [
19098 { name: 'Bob', title: 'CEO' },
19099 { name: 'Frank', title: 'Lowly Developer' }
19100 ];
19101
19102 $scope.columnDefs = [
19103 {name: 'name', enableCellEdit: true},
19104 {name: 'title', enableCellEdit: true}
19105 ];
19106
19107 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
19108 }]);
19109 </file>
19110 <file name="index.html">
19111 <div ng-controller="MainCtrl">
19112 <div ui-grid="gridOptions" ui-grid-grouping></div>
19113 </div>
19114 </file>
19115 </example>
19116 */
19117 module.directive('uiGridGrouping', ['uiGridGroupingConstants', 'uiGridGroupingService', '$templateCache',
19118 function (uiGridGroupingConstants, uiGridGroupingService, $templateCache) {
19119 return {
19120 replace: true,
19121 priority: 0,
19122 require: '^uiGrid',
19123 scope: false,
19124 compile: function () {
19125 return {
19126 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
19127 if (uiGridCtrl.grid.options.enableGrouping !== false){
19128 uiGridGroupingService.initializeGrid(uiGridCtrl.grid, $scope);
19129 }
19130 },
19131 post: function ($scope, $elm, $attrs, uiGridCtrl) {
19132 }
19133 };
19134 }
19135 };
19136 }]);
19137
19138 })();
19139
19140 (function () {
19141 'use strict';
19142
19143 /**
19144 * @ngdoc overview
19145 * @name ui.grid.importer
19146 * @description
19147 *
19148 * # ui.grid.importer
19149 *
19150 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
19151 *
19152 * This module provides the ability to import data into the grid. It
19153 * uses the column defs to work out which data belongs in which column,
19154 * and creates entities from a configured class (typically a $resource).
19155 *
19156 * If the rowEdit feature is enabled, it also calls save on those newly
19157 * created objects, and then displays any errors in the imported data.
19158 *
19159 * Currently the importer imports only CSV and json files, although provision has been
19160 * made to process other file formats, and these can be added over time.
19161 *
19162 * For json files, the properties within each object in the json must match the column names
19163 * (to put it another way, the importer doesn't process the json, it just copies the objects
19164 * within the json into a new instance of the specified object type)
19165 *
19166 * For CSV import, the default column identification relies on each column in the
19167 * header row matching a column.name or column.displayName. Optionally, a column identification
19168 * callback can be used. This allows matching using other attributes, which is particularly
19169 * useful if your application has internationalised column headings (i.e. the headings that
19170 * the user sees don't match the column names).
19171 *
19172 * The importer makes use of the grid menu as the UI for requesting an
19173 * import.
19174 *
19175 * <div ui-grid-importer></div>
19176 */
19177
19178 var module = angular.module('ui.grid.importer', ['ui.grid']);
19179
19180 /**
19181 * @ngdoc object
19182 * @name ui.grid.importer.constant:uiGridImporterConstants
19183 *
19184 * @description constants available in importer module
19185 */
19186
19187 module.constant('uiGridImporterConstants', {
19188 featureName: 'importer'
19189 });
19190
19191 /**
19192 * @ngdoc service
19193 * @name ui.grid.importer.service:uiGridImporterService
19194 *
19195 * @description Services for importer feature
19196 */
19197 module.service('uiGridImporterService', ['$q', 'uiGridConstants', 'uiGridImporterConstants', 'gridUtil', '$compile', '$interval', 'i18nService', '$window',
19198 function ($q, uiGridConstants, uiGridImporterConstants, gridUtil, $compile, $interval, i18nService, $window) {
19199
19200 var service = {
19201
19202 initializeGrid: function ($scope, grid) {
19203
19204 //add feature namespace and any properties to grid for needed state
19205 grid.importer = {
19206 $scope: $scope
19207 };
19208
19209 this.defaultGridOptions(grid.options);
19210
19211 /**
19212 * @ngdoc object
19213 * @name ui.grid.importer.api:PublicApi
19214 *
19215 * @description Public Api for importer feature
19216 */
19217 var publicApi = {
19218 events: {
19219 importer: {
19220 }
19221 },
19222 methods: {
19223 importer: {
19224 /**
19225 * @ngdoc function
19226 * @name importFile
19227 * @methodOf ui.grid.importer.api:PublicApi
19228 * @description Imports a file into the grid using the file object
19229 * provided. Bypasses the grid menu
19230 * @param {File} fileObject the file we want to import, as a javascript
19231 * File object
19232 */
19233 importFile: function ( fileObject ) {
19234 service.importThisFile( grid, fileObject );
19235 }
19236 }
19237 }
19238 };
19239
19240 grid.api.registerEventsFromObject(publicApi.events);
19241
19242 grid.api.registerMethodsFromObject(publicApi.methods);
19243
19244 if ( grid.options.enableImporter && grid.options.importerShowMenu ){
19245 if ( grid.api.core.addToGridMenu ){
19246 service.addToMenu( grid );
19247 } else {
19248 // order of registration is not guaranteed, register in a little while
19249 $interval( function() {
19250 if (grid.api.core.addToGridMenu){
19251 service.addToMenu( grid );
19252 }
19253 }, 100, 1);
19254 }
19255 }
19256 },
19257
19258
19259 defaultGridOptions: function (gridOptions) {
19260 //default option to true unless it was explicitly set to false
19261 /**
19262 * @ngdoc object
19263 * @name ui.grid.importer.api:GridOptions
19264 *
19265 * @description GridOptions for importer feature, these are available to be
19266 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
19267 */
19268
19269 /**
19270 * @ngdoc property
19271 * @propertyOf ui.grid.importer.api:GridOptions
19272 * @name enableImporter
19273 * @description Whether or not importer is enabled. Automatically set
19274 * to false if the user's browser does not support the required fileApi.
19275 * Otherwise defaults to true.
19276 *
19277 */
19278 if (gridOptions.enableImporter || gridOptions.enableImporter === undefined) {
19279 if ( !($window.hasOwnProperty('File') && $window.hasOwnProperty('FileReader') && $window.hasOwnProperty('FileList') && $window.hasOwnProperty('Blob')) ) {
19280 gridUtil.logError('The File APIs are not fully supported in this browser, grid importer cannot be used.');
19281 gridOptions.enableImporter = false;
19282 } else {
19283 gridOptions.enableImporter = true;
19284 }
19285 } else {
19286 gridOptions.enableImporter = false;
19287 }
19288
19289 /**
19290 * @ngdoc method
19291 * @name importerProcessHeaders
19292 * @methodOf ui.grid.importer.api:GridOptions
19293 * @description A callback function that will process headers using custom
19294 * logic. Set this callback function if the headers that your user will provide in their
19295 * import file don't necessarily match the grid header or field names. This might commonly
19296 * occur where your application is internationalised, and therefore the field names
19297 * that the user recognises are in a different language than the field names that
19298 * ui-grid knows about.
19299 *
19300 * Defaults to the internal `processHeaders` method, which seeks to match using both
19301 * displayName and column.name. Any non-matching columns are discarded.
19302 *
19303 * Your callback routine should respond by processing the header array, and returning an array
19304 * of matching column names. A null value in any given position means "don't import this column"
19305 *
19306 * <pre>
19307 * gridOptions.importerProcessHeaders: function( headerArray ) {
19308 * var myHeaderColumns = [];
19309 * var thisCol;
19310 * headerArray.forEach( function( value, index ) {
19311 * thisCol = mySpecialLookupFunction( value );
19312 * myHeaderColumns.push( thisCol.name );
19313 * });
19314 *
19315 * return myHeaderCols;
19316 * })
19317 * </pre>
19318 * @param {Grid} grid the grid we're importing into
19319 * @param {array} headerArray an array of the text from the first row of the csv file,
19320 * which you need to match to column.names
19321 * @returns {array} array of matching column names, in the same order as the headerArray
19322 *
19323 */
19324 gridOptions.importerProcessHeaders = gridOptions.importerProcessHeaders || service.processHeaders;
19325
19326 /**
19327 * @ngdoc method
19328 * @name importerHeaderFilter
19329 * @methodOf ui.grid.importer.api:GridOptions
19330 * @description A callback function that will filter (usually translate) a single
19331 * header. Used when you want to match the passed in column names to the column
19332 * displayName after the header filter.
19333 *
19334 * Your callback routine needs to return the filtered header value.
19335 * <pre>
19336 * gridOptions.importerHeaderFilter: function( displayName ) {
19337 * return $translate.instant( displayName );
19338 * })
19339 * </pre>
19340 *
19341 * or:
19342 * <pre>
19343 * gridOptions.importerHeaderFilter: $translate.instant
19344 * </pre>
19345 * @param {string} displayName the displayName that we'd like to translate
19346 * @returns {string} the translated name
19347 *
19348 */
19349 gridOptions.importerHeaderFilter = gridOptions.importerHeaderFilter || function( displayName ) { return displayName; };
19350
19351 /**
19352 * @ngdoc method
19353 * @name importerErrorCallback
19354 * @methodOf ui.grid.importer.api:GridOptions
19355 * @description A callback function that provides custom error handling, rather
19356 * than the standard grid behaviour of an alert box and a console message. You
19357 * might use this to internationalise the console log messages, or to write to a
19358 * custom logging routine that returned errors to the server.
19359 *
19360 * <pre>
19361 * gridOptions.importerErrorCallback: function( grid, errorKey, consoleMessage, context ) {
19362 * myUserDisplayRoutine( errorKey );
19363 * myLoggingRoutine( consoleMessage, context );
19364 * })
19365 * </pre>
19366 * @param {Grid} grid the grid we're importing into, may be useful if you're positioning messages
19367 * in some way
19368 * @param {string} errorKey one of the i18n keys the importer can return - importer.noHeaders,
19369 * importer.noObjects, importer.invalidCsv, importer.invalidJson, importer.jsonNotArray
19370 * @param {string} consoleMessage the English console message that importer would have written
19371 * @param {object} context the context data that importer would have appended to that console message,
19372 * often the file content itself or the element that is in error
19373 *
19374 */
19375 if ( !gridOptions.importerErrorCallback || typeof(gridOptions.importerErrorCallback) !== 'function' ){
19376 delete gridOptions.importerErrorCallback;
19377 }
19378
19379 /**
19380 * @ngdoc method
19381 * @name importerDataAddCallback
19382 * @methodOf ui.grid.importer.api:GridOptions
19383 * @description A mandatory callback function that adds data to the source data array. The grid
19384 * generally doesn't add rows to the source data array, it is tidier to handle this through a user
19385 * callback.
19386 *
19387 * <pre>
19388 * gridOptions.importerDataAddCallback: function( grid, newObjects ) {
19389 * $scope.myData = $scope.myData.concat( newObjects );
19390 * })
19391 * </pre>
19392 * @param {Grid} grid the grid we're importing into, may be useful in some way
19393 * @param {array} newObjects an array of new objects that you should add to your data
19394 *
19395 */
19396 if ( gridOptions.enableImporter === true && !gridOptions.importerDataAddCallback ) {
19397 gridUtil.logError("You have not set an importerDataAddCallback, importer is disabled");
19398 gridOptions.enableImporter = false;
19399 }
19400
19401 /**
19402 * @ngdoc object
19403 * @name importerNewObject
19404 * @propertyOf ui.grid.importer.api:GridOptions
19405 * @description An object on which we call `new` to create each new row before inserting it into
19406 * the data array. Typically this would be a $resource entity, which means that if you're using
19407 * the rowEdit feature, you can directly call save on this entity when the save event is triggered.
19408 *
19409 * Defaults to a vanilla javascript object
19410 *
19411 * @example
19412 * <pre>
19413 * gridOptions.importerNewObject = MyRes;
19414 * </pre>
19415 *
19416 */
19417
19418 /**
19419 * @ngdoc property
19420 * @propertyOf ui.grid.importer.api:GridOptions
19421 * @name importerShowMenu
19422 * @description Whether or not to show an item in the grid menu. Defaults to true.
19423 *
19424 */
19425 gridOptions.importerShowMenu = gridOptions.importerShowMenu !== false;
19426
19427 /**
19428 * @ngdoc method
19429 * @methodOf ui.grid.importer.api:GridOptions
19430 * @name importerObjectCallback
19431 * @description A callback that massages the data for each object. For example,
19432 * you might have data stored as a code value, but display the decode. This callback
19433 * can be used to change the decoded value back into a code. Defaults to doing nothing.
19434 * @param {Grid} grid in case you need it
19435 * @param {object} newObject the new object as importer has created it, modify it
19436 * then return the modified version
19437 * @returns {object} the modified object
19438 * @example
19439 * <pre>
19440 * gridOptions.importerObjectCallback = function ( grid, newObject ) {
19441 * switch newObject.status {
19442 * case 'Active':
19443 * newObject.status = 1;
19444 * break;
19445 * case 'Inactive':
19446 * newObject.status = 2;
19447 * break;
19448 * }
19449 * return newObject;
19450 * };
19451 * </pre>
19452 */
19453 gridOptions.importerObjectCallback = gridOptions.importerObjectCallback || function( grid, newObject ) { return newObject; };
19454 },
19455
19456
19457 /**
19458 * @ngdoc function
19459 * @name addToMenu
19460 * @methodOf ui.grid.importer.service:uiGridImporterService
19461 * @description Adds import menu item to the grid menu,
19462 * allowing the user to request import of a file
19463 * @param {Grid} grid the grid into which data should be imported
19464 */
19465 addToMenu: function ( grid ) {
19466 grid.api.core.addToGridMenu( grid, [
19467 {
19468 title: i18nService.getSafeText('gridMenu.importerTitle'),
19469 order: 150
19470 },
19471 {
19472 templateUrl: 'ui-grid/importerMenuItemContainer',
19473 action: function ($event) {
19474 this.grid.api.importer.importAFile( grid );
19475 },
19476 order: 151
19477 }
19478 ]);
19479 },
19480
19481
19482 /**
19483 * @ngdoc function
19484 * @name importThisFile
19485 * @methodOf ui.grid.importer.service:uiGridImporterService
19486 * @description Imports the provided file into the grid using the file object
19487 * provided. Bypasses the grid menu
19488 * @param {Grid} grid the grid we're importing into
19489 * @param {File} fileObject the file we want to import, as returned from the File
19490 * javascript object
19491 */
19492 importThisFile: function ( grid, fileObject ) {
19493 if (!fileObject){
19494 gridUtil.logError( 'No file object provided to importThisFile, should be impossible, aborting');
19495 return;
19496 }
19497
19498 var reader = new FileReader();
19499
19500 switch ( fileObject.type ){
19501 case 'application/json':
19502 reader.onload = service.importJsonClosure( grid );
19503 break;
19504 default:
19505 reader.onload = service.importCsvClosure( grid );
19506 break;
19507 }
19508
19509 reader.readAsText( fileObject );
19510 },
19511
19512
19513 /**
19514 * @ngdoc function
19515 * @name importJson
19516 * @methodOf ui.grid.importer.service:uiGridImporterService
19517 * @description Creates a function that imports a json file into the grid.
19518 * The json data is imported into new objects of type `gridOptions.importerNewObject`,
19519 * and if the rowEdit feature is enabled the rows are marked as dirty
19520 * @param {Grid} grid the grid we want to import into
19521 * @param {FileObject} importFile the file that we want to import, as
19522 * a FileObject
19523 */
19524 importJsonClosure: function( grid ) {
19525 return function( importFile ){
19526 var newObjects = [];
19527 var newObject;
19528
19529 var importArray = service.parseJson( grid, importFile );
19530 if (importArray === null){
19531 return;
19532 }
19533 importArray.forEach( function( value, index ) {
19534 newObject = service.newObject( grid );
19535 angular.extend( newObject, value );
19536 newObject = grid.options.importerObjectCallback( grid, newObject );
19537 newObjects.push( newObject );
19538 });
19539
19540 service.addObjects( grid, newObjects );
19541
19542 };
19543 },
19544
19545
19546 /**
19547 * @ngdoc function
19548 * @name parseJson
19549 * @methodOf ui.grid.importer.service:uiGridImporterService
19550 * @description Parses a json file, returns the parsed data.
19551 * Displays an error if file doesn't parse
19552 * @param {Grid} grid the grid that we want to import into
19553 * @param {FileObject} importFile the file that we want to import, as
19554 * a FileObject
19555 * @returns {array} array of objects from the imported json
19556 */
19557 parseJson: function( grid, importFile ){
19558 var loadedObjects;
19559 try {
19560 loadedObjects = JSON.parse( importFile.target.result );
19561 } catch (e) {
19562 service.alertError( grid, 'importer.invalidJson', 'File could not be processed, is it valid json? Content was: ', importFile.target.result );
19563 return;
19564 }
19565
19566 if ( !Array.isArray( loadedObjects ) ){
19567 service.alertError( grid, 'importer.jsonNotarray', 'Import failed, file is not an array, file was: ', importFile.target.result );
19568 return [];
19569 } else {
19570 return loadedObjects;
19571 }
19572 },
19573
19574
19575
19576 /**
19577 * @ngdoc function
19578 * @name importCsvClosure
19579 * @methodOf ui.grid.importer.service:uiGridImporterService
19580 * @description Creates a function that imports a csv file into the grid
19581 * (allowing it to be used in the reader.onload event)
19582 * @param {Grid} grid the grid that we want to import into
19583 * @param {FileObject} importFile the file that we want to import, as
19584 * a file object
19585 */
19586 importCsvClosure: function( grid ) {
19587 return function( importFile ){
19588 var importArray = service.parseCsv( importFile );
19589 if ( !importArray || importArray.length < 1 ){
19590 service.alertError( grid, 'importer.invalidCsv', 'File could not be processed, is it valid csv? Content was: ', importFile.target.result );
19591 return;
19592 }
19593
19594 var newObjects = service.createCsvObjects( grid, importArray );
19595 if ( !newObjects || newObjects.length === 0 ){
19596 service.alertError( grid, 'importer.noObjects', 'Objects were not able to be derived, content was: ', importFile.target.result );
19597 return;
19598 }
19599
19600 service.addObjects( grid, newObjects );
19601 };
19602 },
19603
19604
19605 /**
19606 * @ngdoc function
19607 * @name parseCsv
19608 * @methodOf ui.grid.importer.service:uiGridImporterService
19609 * @description Parses a csv file into an array of arrays, with the first
19610 * array being the headers, and the remaining arrays being the data.
19611 * The logic for this comes from https://github.com/thetalecrafter/excel.js/blob/master/src/csv.js,
19612 * which is noted as being under the MIT license. The code is modified to pass the jscs yoda condition
19613 * checker
19614 * @param {FileObject} importFile the file that we want to import, as a
19615 * file object
19616 */
19617 parseCsv: function( importFile ) {
19618 var csv = importFile.target.result;
19619
19620 // use the CSV-JS library to parse
19621 return CSV.parse(csv);
19622 },
19623
19624
19625 /**
19626 * @ngdoc function
19627 * @name createCsvObjects
19628 * @methodOf ui.grid.importer.service:uiGridImporterService
19629 * @description Converts an array of arrays (representing the csv file)
19630 * into a set of objects. Uses the provided `gridOptions.importerNewObject`
19631 * to create the objects, and maps the header row into the individual columns
19632 * using either `gridOptions.importerProcessHeaders`, or by using a native method
19633 * of matching to either the displayName, column name or column field of
19634 * the columns in the column defs. The resulting objects will have attributes
19635 * that are named based on the column.field or column.name, in that order.
19636 * @param {Grid} grid the grid that we want to import into
19637 * @param {Array} importArray the data that we want to import, as an array
19638 */
19639 createCsvObjects: function( grid, importArray ){
19640 // pull off header row and turn into headers
19641 var headerMapping = grid.options.importerProcessHeaders( grid, importArray.shift() );
19642 if ( !headerMapping || headerMapping.length === 0 ){
19643 service.alertError( grid, 'importer.noHeaders', 'Column names could not be derived, content was: ', importArray );
19644 return [];
19645 }
19646
19647 var newObjects = [];
19648 var newObject;
19649 importArray.forEach( function( row, index ) {
19650 newObject = service.newObject( grid );
19651 if ( row !== null ){
19652 row.forEach( function( field, index ){
19653 if ( headerMapping[index] !== null ){
19654 newObject[ headerMapping[index] ] = field;
19655 }
19656 });
19657 }
19658 newObject = grid.options.importerObjectCallback( grid, newObject );
19659 newObjects.push( newObject );
19660 });
19661
19662 return newObjects;
19663 },
19664
19665
19666 /**
19667 * @ngdoc function
19668 * @name processHeaders
19669 * @methodOf ui.grid.importer.service:uiGridImporterService
19670 * @description Determines the columns that the header row from
19671 * a csv (or other) file represents.
19672 * @param {Grid} grid the grid we're importing into
19673 * @param {array} headerRow the header row that we wish to match against
19674 * the column definitions
19675 * @returns {array} an array of the attribute names that should be used
19676 * for that column, based on matching the headers or creating the headers
19677 *
19678 */
19679 processHeaders: function( grid, headerRow ) {
19680 var headers = [];
19681 if ( !grid.options.columnDefs || grid.options.columnDefs.length === 0 ){
19682 // we are going to create new columnDefs for all these columns, so just remove
19683 // spaces from the names to create fields
19684 headerRow.forEach( function( value, index ) {
19685 headers.push( value.replace( /[^0-9a-zA-Z\-_]/g, '_' ) );
19686 });
19687 return headers;
19688 } else {
19689 var lookupHash = service.flattenColumnDefs( grid, grid.options.columnDefs );
19690 headerRow.forEach( function( value, index ) {
19691 if ( lookupHash[value] ) {
19692 headers.push( lookupHash[value] );
19693 } else if ( lookupHash[ value.toLowerCase() ] ) {
19694 headers.push( lookupHash[ value.toLowerCase() ] );
19695 } else {
19696 headers.push( null );
19697 }
19698 });
19699 return headers;
19700 }
19701 },
19702
19703
19704 /**
19705 * @name flattenColumnDefs
19706 * @methodOf ui.grid.importer.service:uiGridImporterService
19707 * @description Runs through the column defs and creates a hash of
19708 * the displayName, name and field, and of each of those values forced to lower case,
19709 * with each pointing to the field or name
19710 * (whichever is present). Used to lookup column headers and decide what
19711 * attribute name to give to the resulting field.
19712 * @param {Grid} grid the grid we're importing into
19713 * @param {array} columnDefs the columnDefs that we should flatten
19714 * @returns {hash} the flattened version of the column def information, allowing
19715 * us to look up a value by `flattenedHash[ headerValue ]`
19716 */
19717 flattenColumnDefs: function( grid, columnDefs ){
19718 var flattenedHash = {};
19719 columnDefs.forEach( function( columnDef, index) {
19720 if ( columnDef.name ){
19721 flattenedHash[ columnDef.name ] = columnDef.field || columnDef.name;
19722 flattenedHash[ columnDef.name.toLowerCase() ] = columnDef.field || columnDef.name;
19723 }
19724
19725 if ( columnDef.field ){
19726 flattenedHash[ columnDef.field ] = columnDef.field || columnDef.name;
19727 flattenedHash[ columnDef.field.toLowerCase() ] = columnDef.field || columnDef.name;
19728 }
19729
19730 if ( columnDef.displayName ){
19731 flattenedHash[ columnDef.displayName ] = columnDef.field || columnDef.name;
19732 flattenedHash[ columnDef.displayName.toLowerCase() ] = columnDef.field || columnDef.name;
19733 }
19734
19735 if ( columnDef.displayName && grid.options.importerHeaderFilter ){
19736 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName) ] = columnDef.field || columnDef.name;
19737 flattenedHash[ grid.options.importerHeaderFilter(columnDef.displayName).toLowerCase() ] = columnDef.field || columnDef.name;
19738 }
19739 });
19740
19741 return flattenedHash;
19742 },
19743
19744
19745 /**
19746 * @ngdoc function
19747 * @name addObjects
19748 * @methodOf ui.grid.importer.service:uiGridImporterService
19749 * @description Inserts our new objects into the grid data, and
19750 * sets the rows to dirty if the rowEdit feature is being used
19751 *
19752 * Does this by registering a watch on dataChanges, which essentially
19753 * is waiting on the result of the grid data watch, and downstream processing.
19754 *
19755 * When the callback is called, it deregisters itself - we don't want to run
19756 * again next time data is added.
19757 *
19758 * If we never get called, we deregister on destroy.
19759 *
19760 * @param {Grid} grid the grid we're importing into
19761 * @param {array} newObjects the objects we want to insert into the grid data
19762 * @returns {object} the new object
19763 */
19764 addObjects: function( grid, newObjects, $scope ){
19765 if ( grid.api.rowEdit ){
19766 var dataChangeDereg = grid.registerDataChangeCallback( function() {
19767 grid.api.rowEdit.setRowsDirty( newObjects );
19768 dataChangeDereg();
19769 }, [uiGridConstants.dataChange.ROW] );
19770
19771 grid.importer.$scope.$on( '$destroy', dataChangeDereg );
19772 }
19773
19774 grid.importer.$scope.$apply( grid.options.importerDataAddCallback( grid, newObjects ) );
19775
19776 },
19777
19778
19779 /**
19780 * @ngdoc function
19781 * @name newObject
19782 * @methodOf ui.grid.importer.service:uiGridImporterService
19783 * @description Makes a new object based on `gridOptions.importerNewObject`,
19784 * or based on an empty object if not present
19785 * @param {Grid} grid the grid we're importing into
19786 * @returns {object} the new object
19787 */
19788 newObject: function( grid ){
19789 if ( typeof(grid.options) !== "undefined" && typeof(grid.options.importerNewObject) !== "undefined" ){
19790 return new grid.options.importerNewObject();
19791 } else {
19792 return {};
19793 }
19794 },
19795
19796
19797 /**
19798 * @ngdoc function
19799 * @name alertError
19800 * @methodOf ui.grid.importer.service:uiGridImporterService
19801 * @description Provides an internationalised user alert for the failure,
19802 * and logs a console message including diagnostic content.
19803 * Optionally, if the the `gridOptions.importerErrorCallback` routine
19804 * is defined, then calls that instead, allowing user specified error routines
19805 * @param {Grid} grid the grid we're importing into
19806 * @param {array} headerRow the header row that we wish to match against
19807 * the column definitions
19808 */
19809 alertError: function( grid, alertI18nToken, consoleMessage, context ){
19810 if ( grid.options.importerErrorCallback ){
19811 grid.options.importerErrorCallback( grid, alertI18nToken, consoleMessage, context );
19812 } else {
19813 $window.alert(i18nService.getSafeText( alertI18nToken ));
19814 gridUtil.logError(consoleMessage + context );
19815 }
19816 }
19817 };
19818
19819 return service;
19820
19821 }
19822 ]);
19823
19824 /**
19825 * @ngdoc directive
19826 * @name ui.grid.importer.directive:uiGridImporter
19827 * @element div
19828 * @restrict A
19829 *
19830 * @description Adds importer features to grid
19831 *
19832 */
19833 module.directive('uiGridImporter', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
19834 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
19835 return {
19836 replace: true,
19837 priority: 0,
19838 require: '^uiGrid',
19839 scope: false,
19840 link: function ($scope, $elm, $attrs, uiGridCtrl) {
19841 uiGridImporterService.initializeGrid($scope, uiGridCtrl.grid);
19842 }
19843 };
19844 }
19845 ]);
19846
19847 /**
19848 * @ngdoc directive
19849 * @name ui.grid.importer.directive:uiGridImporterMenuItem
19850 * @element div
19851 * @restrict A
19852 *
19853 * @description Handles the processing from the importer menu item - once a file is
19854 * selected
19855 *
19856 */
19857 module.directive('uiGridImporterMenuItem', ['uiGridImporterConstants', 'uiGridImporterService', 'gridUtil', '$compile',
19858 function (uiGridImporterConstants, uiGridImporterService, gridUtil, $compile) {
19859 return {
19860 replace: true,
19861 priority: 0,
19862 require: '^uiGrid',
19863 scope: false,
19864 templateUrl: 'ui-grid/importerMenuItem',
19865 link: function ($scope, $elm, $attrs, uiGridCtrl) {
19866 var handleFileSelect = function( event ){
19867 var target = event.srcElement || event.target;
19868
19869 if (target && target.files && target.files.length === 1) {
19870 var fileObject = target.files[0];
19871 uiGridImporterService.importThisFile( grid, fileObject );
19872 target.form.reset();
19873 }
19874 };
19875
19876 var fileChooser = $elm[0].querySelectorAll('.ui-grid-importer-file-chooser');
19877 var grid = uiGridCtrl.grid;
19878
19879 if ( fileChooser.length !== 1 ){
19880 gridUtil.logError('Found > 1 or < 1 file choosers within the menu item, error, cannot continue');
19881 } else {
19882 fileChooser[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google
19883 }
19884 }
19885 };
19886 }
19887 ]);
19888 })();
19889
19890 (function() {
19891 'use strict';
19892 /**
19893 * @ngdoc overview
19894 * @name ui.grid.infiniteScroll
19895 *
19896 * @description
19897 *
19898 * #ui.grid.infiniteScroll
19899 *
19900 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
19901 *
19902 * This module provides infinite scroll functionality to ui-grid
19903 *
19904 */
19905 var module = angular.module('ui.grid.infiniteScroll', ['ui.grid']);
19906 /**
19907 * @ngdoc service
19908 * @name ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
19909 *
19910 * @description Service for infinite scroll features
19911 */
19912 module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', '$q', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent, $q) {
19913
19914 var service = {
19915
19916 /**
19917 * @ngdoc function
19918 * @name initializeGrid
19919 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
19920 * @description This method register events and methods into grid public API
19921 */
19922
19923 initializeGrid: function(grid, $scope) {
19924 service.defaultGridOptions(grid.options);
19925
19926 if (!grid.options.enableInfiniteScroll){
19927 return;
19928 }
19929
19930 grid.infiniteScroll = { dataLoading: false };
19931 service.setScrollDirections( grid, grid.options.infiniteScrollUp, grid.options.infiniteScrollDown );
19932 grid.api.core.on.scrollEnd($scope, service.handleScroll);
19933
19934 /**
19935 * @ngdoc object
19936 * @name ui.grid.infiniteScroll.api:PublicAPI
19937 *
19938 * @description Public API for infinite scroll feature
19939 */
19940 var publicApi = {
19941 events: {
19942 infiniteScroll: {
19943
19944 /**
19945 * @ngdoc event
19946 * @name needLoadMoreData
19947 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
19948 * @description This event fires when scroll reaches bottom percentage of grid
19949 * and needs to load data
19950 */
19951
19952 needLoadMoreData: function ($scope, fn) {
19953 },
19954
19955 /**
19956 * @ngdoc event
19957 * @name needLoadMoreDataTop
19958 * @eventOf ui.grid.infiniteScroll.api:PublicAPI
19959 * @description This event fires when scroll reaches top percentage of grid
19960 * and needs to load data
19961 */
19962
19963 needLoadMoreDataTop: function ($scope, fn) {
19964 }
19965 }
19966 },
19967 methods: {
19968 infiniteScroll: {
19969
19970 /**
19971 * @ngdoc function
19972 * @name dataLoaded
19973 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
19974 * @description Call this function when you have loaded the additional data
19975 * requested. You should set scrollUp and scrollDown to indicate
19976 * whether there are still more pages in each direction.
19977 *
19978 * If you call dataLoaded without first calling `saveScrollPercentage` then we will
19979 * scroll the user to the start of the newly loaded data, which usually gives a smooth scroll
19980 * experience, but can give a jumpy experience with large `infiniteScrollRowsFromEnd` values, and
19981 * on variable speed internet connections. Using `saveScrollPercentage` as demonstrated in the tutorial
19982 * should give a smoother scrolling experience for users.
19983 *
19984 * See infinite_scroll tutorial for example of usage
19985 * @param {boolean} scrollUp if set to false flags that there are no more pages upwards, so don't fire
19986 * any more infinite scroll events upward
19987 * @param {boolean} scrollDown if set to false flags that there are no more pages downwards, so don't
19988 * fire any more infinite scroll events downward
19989 * @returns {promise} a promise that is resolved when the grid scrolling is fully adjusted. If you're
19990 * planning to remove pages, you should wait on this promise first, or you'll break the scroll positioning
19991 */
19992 dataLoaded: function( scrollUp, scrollDown ) {
19993 service.setScrollDirections(grid, scrollUp, scrollDown);
19994
19995 var promise = service.adjustScroll(grid).then(function() {
19996 grid.infiniteScroll.dataLoading = false;
19997 });
19998
19999 return promise;
20000 },
20001
20002 /**
20003 * @ngdoc function
20004 * @name resetScroll
20005 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20006 * @description Call this function when you have taken some action that makes the current
20007 * scroll position invalid. For example, if you're using external sorting and you've resorted
20008 * then you might reset the scroll, or if you've otherwise substantially changed the data, perhaps
20009 * you've reused an existing grid for a new data set
20010 *
20011 * You must tell us whether there is data upwards or downwards after the reset
20012 *
20013 * @param {boolean} scrollUp flag that there are pages upwards, fire
20014 * infinite scroll events upward
20015 * @param {boolean} scrollDown flag that there are pages downwards, so
20016 * fire infinite scroll events downward
20017 * @returns {promise} promise that is resolved when the scroll reset is complete
20018 */
20019 resetScroll: function( scrollUp, scrollDown ) {
20020 service.setScrollDirections( grid, scrollUp, scrollDown);
20021
20022 return service.adjustInfiniteScrollPosition(grid, 0);
20023 },
20024
20025
20026 /**
20027 * @ngdoc function
20028 * @name saveScrollPercentage
20029 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20030 * @description Saves the scroll percentage and number of visible rows before you adjust the data,
20031 * used if you're subsequently going to call `dataRemovedTop` or `dataRemovedBottom`
20032 */
20033 saveScrollPercentage: function() {
20034 grid.infiniteScroll.prevScrollTop = grid.renderContainers.body.prevScrollTop;
20035 grid.infiniteScroll.previousVisibleRows = grid.getVisibleRowCount();
20036 },
20037
20038
20039 /**
20040 * @ngdoc function
20041 * @name dataRemovedTop
20042 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20043 * @description Adjusts the scroll position after you've removed data at the top
20044 * @param {boolean} scrollUp flag that there are pages upwards, fire
20045 * infinite scroll events upward
20046 * @param {boolean} scrollDown flag that there are pages downwards, so
20047 * fire infinite scroll events downward
20048 */
20049 dataRemovedTop: function( scrollUp, scrollDown ) {
20050 service.dataRemovedTop( grid, scrollUp, scrollDown );
20051 },
20052
20053 /**
20054 * @ngdoc function
20055 * @name dataRemovedBottom
20056 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20057 * @description Adjusts the scroll position after you've removed data at the bottom
20058 * @param {boolean} scrollUp flag that there are pages upwards, fire
20059 * infinite scroll events upward
20060 * @param {boolean} scrollDown flag that there are pages downwards, so
20061 * fire infinite scroll events downward
20062 */
20063 dataRemovedBottom: function( scrollUp, scrollDown ) {
20064 service.dataRemovedBottom( grid, scrollUp, scrollDown );
20065 },
20066
20067 /**
20068 * @ngdoc function
20069 * @name setScrollDirections
20070 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20071 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20072 * and also sets the grid.suppressParentScroll
20073 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20074 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20075 */
20076 setScrollDirections: function ( scrollUp, scrollDown ) {
20077 service.setScrollDirections( grid, scrollUp, scrollDown );
20078 }
20079
20080 }
20081 }
20082 };
20083 grid.api.registerEventsFromObject(publicApi.events);
20084 grid.api.registerMethodsFromObject(publicApi.methods);
20085 },
20086
20087
20088 defaultGridOptions: function (gridOptions) {
20089 //default option to true unless it was explicitly set to false
20090 /**
20091 * @ngdoc object
20092 * @name ui.grid.infiniteScroll.api:GridOptions
20093 *
20094 * @description GridOptions for infinite scroll feature, these are available to be
20095 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
20096 */
20097
20098 /**
20099 * @ngdoc object
20100 * @name enableInfiniteScroll
20101 * @propertyOf ui.grid.infiniteScroll.api:GridOptions
20102 * @description Enable infinite scrolling for this grid
20103 * <br/>Defaults to true
20104 */
20105 gridOptions.enableInfiniteScroll = gridOptions.enableInfiniteScroll !== false;
20106
20107 /**
20108 * @ngdoc property
20109 * @name infiniteScrollRowsFromEnd
20110 * @propertyOf ui.grid.class:GridOptions
20111 * @description This setting controls how close to the end of the dataset a user gets before
20112 * more data is requested by the infinite scroll, whether scrolling up or down. This allows you to
20113 * 'prefetch' rows before the user actually runs out of scrolling.
20114 *
20115 * Note that if you set this value too high it may give jumpy scrolling behaviour, if you're getting
20116 * this behaviour you could use the `saveScrollPercentageMethod` right before loading your data, and we'll
20117 * preserve that scroll position
20118 *
20119 * <br> Defaults to 20
20120 */
20121 gridOptions.infiniteScrollRowsFromEnd = gridOptions.infiniteScrollRowsFromEnd || 20;
20122
20123 /**
20124 * @ngdoc property
20125 * @name infiniteScrollUp
20126 * @propertyOf ui.grid.class:GridOptions
20127 * @description Whether you allow infinite scroll up, implying that the first page of data
20128 * you have displayed is in the middle of your data set. If set to true then we trigger the
20129 * needMoreDataTop event when the user hits the top of the scrollbar.
20130 * <br> Defaults to false
20131 */
20132 gridOptions.infiniteScrollUp = gridOptions.infiniteScrollUp === true;
20133
20134 /**
20135 * @ngdoc property
20136 * @name infiniteScrollDown
20137 * @propertyOf ui.grid.class:GridOptions
20138 * @description Whether you allow infinite scroll down, implying that the first page of data
20139 * you have displayed is in the middle of your data set. If set to true then we trigger the
20140 * needMoreData event when the user hits the bottom of the scrollbar.
20141 * <br> Defaults to true
20142 */
20143 gridOptions.infiniteScrollDown = gridOptions.infiniteScrollDown !== false;
20144 },
20145
20146
20147 /**
20148 * @ngdoc function
20149 * @name setScrollDirections
20150 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20151 * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined,
20152 * and also sets the grid.suppressParentScroll
20153 * @param {grid} grid the grid we're operating on
20154 * @param {boolean} scrollUp whether there are pages available up - defaults to false
20155 * @param {boolean} scrollDown whether there are pages available down - defaults to true
20156 */
20157 setScrollDirections: function ( grid, scrollUp, scrollDown ) {
20158 grid.infiniteScroll.scrollUp = ( scrollUp === true );
20159 grid.suppressParentScrollUp = ( scrollUp === true );
20160
20161 grid.infiniteScroll.scrollDown = ( scrollDown !== false);
20162 grid.suppressParentScrollDown = ( scrollDown !== false);
20163 },
20164
20165
20166 /**
20167 * @ngdoc function
20168 * @name handleScroll
20169 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20170 * @description Called whenever the grid scrolls, determines whether the scroll should
20171 * trigger an infinite scroll request for more data
20172 * @param {object} args the args from the event
20173 */
20174 handleScroll: function (args) {
20175 // don't request data if already waiting for data, or if source is coming from ui.grid.adjustInfiniteScrollPosition() function
20176 if ( args.grid.infiniteScroll && args.grid.infiniteScroll.dataLoading || args.source === 'ui.grid.adjustInfiniteScrollPosition' ){
20177 return;
20178 }
20179
20180 if (args.y) {
20181 var percentage;
20182 var targetPercentage = args.grid.options.infiniteScrollRowsFromEnd / args.grid.renderContainers.body.visibleRowCache.length;
20183 if (args.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) {
20184 percentage = args.y.percentage;
20185 if (percentage <= targetPercentage){
20186 service.loadData(args.grid);
20187 }
20188 } else if (args.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN) {
20189 percentage = 1 - args.y.percentage;
20190 if (percentage <= targetPercentage){
20191 service.loadData(args.grid);
20192 }
20193 }
20194 }
20195 },
20196
20197
20198 /**
20199 * @ngdoc function
20200 * @name loadData
20201 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20202 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20203 * and whether there are more pages upwards or downwards. It also stores the number of rows that we had previously,
20204 * and clears out any saved scroll position so that we know whether or not the user calls `saveScrollPercentage`
20205 * @param {Grid} grid the grid we're working on
20206 */
20207 loadData: function (grid) {
20208 // save number of currently visible rows to calculate new scroll position later - we know that we want
20209 // to be at approximately the row we're currently at
20210 grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20211 grid.infiniteScroll.direction = grid.scrollDirection;
20212 delete grid.infiniteScroll.prevScrollTop;
20213
20214 if (grid.scrollDirection === uiGridConstants.scrollDirection.UP && grid.infiniteScroll.scrollUp ) {
20215 grid.infiniteScroll.dataLoading = true;
20216 grid.api.infiniteScroll.raise.needLoadMoreDataTop();
20217 } else if (grid.scrollDirection === uiGridConstants.scrollDirection.DOWN && grid.infiniteScroll.scrollDown ) {
20218 grid.infiniteScroll.dataLoading = true;
20219 grid.api.infiniteScroll.raise.needLoadMoreData();
20220 }
20221 },
20222
20223
20224 /**
20225 * @ngdoc function
20226 * @name adjustScroll
20227 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20228 * @description Once we are informed that data has been loaded, adjust the scroll position to account for that
20229 * addition and to make things look clean.
20230 *
20231 * If we're scrolling up we scroll to the first row of the old data set -
20232 * so we're assuming that you would have gotten to the top of the grid (from the 20% need more data trigger) by
20233 * the time the data comes back. If we're scrolling down we scoll to the last row of the old data set - so we're
20234 * assuming that you would have gotten to the bottom of the grid (from the 80% need more data trigger) by the time
20235 * the data comes back.
20236 *
20237 * Neither of these are good assumptions, but making this a smoother experience really requires
20238 * that trigger to not be a percentage, and to be much closer to the end of the data (say, 5 rows off the end). Even then
20239 * it'd be better still to actually run into the end. But if the data takes a while to come back, they may have scrolled
20240 * somewhere else in the mean-time, in which case they'll get a jump back to the new data. Anyway, this will do for
20241 * now, until someone wants to do better.
20242 * @param {Grid} grid the grid we're working on
20243 * @returns {promise} a promise that is resolved when scrolling has finished
20244 */
20245 adjustScroll: function(grid){
20246 var promise = $q.defer();
20247 $timeout(function () {
20248 var newPercentage, viewportHeight, rowHeight, newVisibleRows, oldTop, newTop;
20249
20250 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight;
20251 rowHeight = grid.options.rowHeight;
20252
20253 if ( grid.infiniteScroll.direction === undefined ){
20254 // called from initialize, tweak our scroll up a little
20255 service.adjustInfiniteScrollPosition(grid, 0);
20256 }
20257
20258 newVisibleRows = grid.getVisibleRowCount();
20259
20260 // in case not enough data is loaded to enable scroller - load more data
20261 var canvasHeight = rowHeight * newVisibleRows;
20262 if (grid.infiniteScroll.scrollDown && (viewportHeight > canvasHeight)) {
20263 grid.api.infiniteScroll.raise.needLoadMoreData();
20264 }
20265
20266 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){
20267 oldTop = grid.infiniteScroll.prevScrollTop || 0;
20268 newTop = oldTop + (newVisibleRows - grid.infiniteScroll.previousVisibleRows)*rowHeight;
20269 service.adjustInfiniteScrollPosition(grid, newTop);
20270 $timeout( function() {
20271 promise.resolve();
20272 });
20273 }
20274
20275 if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){
20276 newTop = grid.infiniteScroll.prevScrollTop || (grid.infiniteScroll.previousVisibleRows*rowHeight - viewportHeight);
20277 service.adjustInfiniteScrollPosition(grid, newTop);
20278 $timeout( function() {
20279 promise.resolve();
20280 });
20281 }
20282 }, 0);
20283
20284 return promise.promise;
20285 },
20286
20287
20288 /**
20289 * @ngdoc function
20290 * @name adjustInfiniteScrollPosition
20291 * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService
20292 * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection
20293 * @param {Grid} grid the grid we're working on
20294 * @param {number} scrollTop the position through the grid that we want to scroll to
20295 * @returns {promise} a promise that is resolved when the scrolling finishes
20296 */
20297 adjustInfiniteScrollPosition: function (grid, scrollTop) {
20298 var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'),
20299 visibleRows = grid.getVisibleRowCount(),
20300 viewportHeight = grid.getViewportHeight() + grid.headerHeight - grid.renderContainers.body.headerHeight - grid.scrollbarHeight,
20301 rowHeight = grid.options.rowHeight,
20302 scrollHeight = visibleRows*rowHeight-viewportHeight;
20303
20304 //for infinite scroll, if there are pages upwards then never allow it to be at the zero position so the up button can be active
20305 if (scrollTop === 0 && grid.infiniteScroll.scrollUp) {
20306 // using pixels results in a relative scroll, hence we have to use percentage
20307 scrollEvent.y = {percentage: 1/scrollHeight};
20308 }
20309 else {
20310 scrollEvent.y = {percentage: scrollTop/scrollHeight};
20311 }
20312 grid.scrollContainers('', scrollEvent);
20313 },
20314
20315
20316 /**
20317 * @ngdoc function
20318 * @name dataRemovedTop
20319 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20320 * @description Adjusts the scroll position after you've removed data at the top. You should
20321 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
20322 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
20323 * before you start removing data
20324 * @param {Grid} grid the grid we're working on
20325 * @param {boolean} scrollUp flag that there are pages upwards, fire
20326 * infinite scroll events upward
20327 * @param {boolean} scrollDown flag that there are pages downwards, so
20328 * fire infinite scroll events downward
20329 * @returns {promise} a promise that is resolved when the scrolling finishes
20330 */
20331 dataRemovedTop: function( grid, scrollUp, scrollDown ) {
20332 var newVisibleRows, oldTop, newTop, rowHeight;
20333 service.setScrollDirections( grid, scrollUp, scrollDown );
20334
20335 newVisibleRows = grid.renderContainers.body.visibleRowCache.length;
20336 oldTop = grid.infiniteScroll.prevScrollTop;
20337 rowHeight = grid.options.rowHeight;
20338
20339 // since we removed from the top, our new scroll row will be the old scroll row less the number
20340 // of rows removed
20341 newTop = oldTop - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows )*rowHeight;
20342
20343 return service.adjustInfiniteScrollPosition( grid, newTop );
20344 },
20345
20346 /**
20347 * @ngdoc function
20348 * @name dataRemovedBottom
20349 * @methodOf ui.grid.infiniteScroll.api:PublicAPI
20350 * @description Adjusts the scroll position after you've removed data at the bottom. You should
20351 * have called `saveScrollPercentage` before you remove the data, and if you're doing this in
20352 * response to a `needMoreData` you should wait until the promise from `loadData` has resolved
20353 * before you start removing data
20354 * @param {Grid} grid the grid we're working on
20355 * @param {boolean} scrollUp flag that there are pages upwards, fire
20356 * infinite scroll events upward
20357 * @param {boolean} scrollDown flag that there are pages downwards, so
20358 * fire infinite scroll events downward
20359 */
20360 dataRemovedBottom: function( grid, scrollUp, scrollDown ) {
20361 var newTop;
20362 service.setScrollDirections( grid, scrollUp, scrollDown );
20363
20364 newTop = grid.infiniteScroll.prevScrollTop;
20365
20366 return service.adjustInfiniteScrollPosition( grid, newTop );
20367 }
20368 };
20369 return service;
20370 }]);
20371 /**
20372 * @ngdoc directive
20373 * @name ui.grid.infiniteScroll.directive:uiGridInfiniteScroll
20374 * @element div
20375 * @restrict A
20376 *
20377 * @description Adds infinite scroll features to grid
20378 *
20379 * @example
20380 <example module="app">
20381 <file name="app.js">
20382 var app = angular.module('app', ['ui.grid', 'ui.grid.infiniteScroll']);
20383
20384 app.controller('MainCtrl', ['$scope', function ($scope) {
20385 $scope.data = [
20386 { name: 'Alex', car: 'Toyota' },
20387 { name: 'Sam', car: 'Lexus' }
20388 ];
20389
20390 $scope.columnDefs = [
20391 {name: 'name'},
20392 {name: 'car'}
20393 ];
20394 }]);
20395 </file>
20396 <file name="index.html">
20397 <div ng-controller="MainCtrl">
20398 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-infinite-scroll="20"></div>
20399 </div>
20400 </file>
20401 </example>
20402 */
20403
20404 module.directive('uiGridInfiniteScroll', ['uiGridInfiniteScrollService',
20405 function (uiGridInfiniteScrollService) {
20406 return {
20407 priority: -200,
20408 scope: false,
20409 require: '^uiGrid',
20410 compile: function($scope, $elm, $attr){
20411 return {
20412 pre: function($scope, $elm, $attr, uiGridCtrl) {
20413 uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid, $scope);
20414 },
20415 post: function($scope, $elm, $attr) {
20416 }
20417 };
20418 }
20419 };
20420 }]);
20421
20422 })();
20423
20424 (function () {
20425 'use strict';
20426
20427 /**
20428 * @ngdoc overview
20429 * @name ui.grid.moveColumns
20430 * @description
20431 *
20432 * # ui.grid.moveColumns
20433 *
20434 * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
20435 *
20436 * This module provides column moving capability to ui.grid. It enables to change the position of columns.
20437 * <div doc-module-components="ui.grid.moveColumns"></div>
20438 */
20439 var module = angular.module('ui.grid.moveColumns', ['ui.grid']);
20440
20441 /**
20442 * @ngdoc service
20443 * @name ui.grid.moveColumns.service:uiGridMoveColumnService
20444 * @description Service for column moving feature.
20445 */
20446 module.service('uiGridMoveColumnService', ['$q', '$timeout', '$log', 'ScrollEvent', 'uiGridConstants', 'gridUtil', function ($q, $timeout, $log, ScrollEvent, uiGridConstants, gridUtil) {
20447
20448 var service = {
20449 initializeGrid: function (grid) {
20450 var self = this;
20451 this.registerPublicApi(grid);
20452 this.defaultGridOptions(grid.options);
20453 grid.moveColumns = {orderCache: []}; // Used to cache the order before columns are rebuilt
20454 grid.registerColumnBuilder(self.movableColumnBuilder);
20455 grid.registerDataChangeCallback(self.verifyColumnOrder, [uiGridConstants.dataChange.COLUMN]);
20456 },
20457 registerPublicApi: function (grid) {
20458 var self = this;
20459 /**
20460 * @ngdoc object
20461 * @name ui.grid.moveColumns.api:PublicApi
20462 * @description Public Api for column moving feature.
20463 */
20464 var publicApi = {
20465 events: {
20466 /**
20467 * @ngdoc event
20468 * @name columnPositionChanged
20469 * @eventOf ui.grid.moveColumns.api:PublicApi
20470 * @description raised when column is moved
20471 * <pre>
20472 * gridApi.colMovable.on.columnPositionChanged(scope,function(colDef, originalPosition, newPosition){})
20473 * </pre>
20474 * @param {object} colDef the column that was moved
20475 * @param {integer} originalPosition of the column
20476 * @param {integer} finalPosition of the column
20477 */
20478 colMovable: {
20479 columnPositionChanged: function (colDef, originalPosition, newPosition) {
20480 }
20481 }
20482 },
20483 methods: {
20484 /**
20485 * @ngdoc method
20486 * @name moveColumn
20487 * @methodOf ui.grid.moveColumns.api:PublicApi
20488 * @description Method can be used to change column position.
20489 * <pre>
20490 * gridApi.colMovable.moveColumn(oldPosition, newPosition)
20491 * </pre>
20492 * @param {integer} originalPosition of the column
20493 * @param {integer} finalPosition of the column
20494 */
20495 colMovable: {
20496 moveColumn: function (originalPosition, finalPosition) {
20497 var columns = grid.columns;
20498 if (!angular.isNumber(originalPosition) || !angular.isNumber(finalPosition)) {
20499 gridUtil.logError('MoveColumn: Please provide valid values for originalPosition and finalPosition');
20500 return;
20501 }
20502 var nonMovableColumns = 0;
20503 for (var i = 0; i < columns.length; i++) {
20504 if ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true) {
20505 nonMovableColumns++;
20506 }
20507 }
20508 if (originalPosition >= (columns.length - nonMovableColumns) || finalPosition >= (columns.length - nonMovableColumns)) {
20509 gridUtil.logError('MoveColumn: Invalid values for originalPosition, finalPosition');
20510 return;
20511 }
20512 var findPositionForRenderIndex = function (index) {
20513 var position = index;
20514 for (var i = 0; i <= position; i++) {
20515 if (angular.isDefined(columns[i]) && ((angular.isDefined(columns[i].colDef.visible) && columns[i].colDef.visible === false) || columns[i].isRowHeader === true)) {
20516 position++;
20517 }
20518 }
20519 return position;
20520 };
20521 self.redrawColumnAtPosition(grid, findPositionForRenderIndex(originalPosition), findPositionForRenderIndex(finalPosition));
20522 }
20523 }
20524 }
20525 };
20526 grid.api.registerEventsFromObject(publicApi.events);
20527 grid.api.registerMethodsFromObject(publicApi.methods);
20528 },
20529 defaultGridOptions: function (gridOptions) {
20530 /**
20531 * @ngdoc object
20532 * @name ui.grid.moveColumns.api:GridOptions
20533 *
20534 * @description Options for configuring the move column feature, these are available to be
20535 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
20536 */
20537 /**
20538 * @ngdoc object
20539 * @name enableColumnMoving
20540 * @propertyOf ui.grid.moveColumns.api:GridOptions
20541 * @description If defined, sets the default value for the colMovable flag on each individual colDefs
20542 * if their individual enableColumnMoving configuration is not defined. Defaults to true.
20543 */
20544 gridOptions.enableColumnMoving = gridOptions.enableColumnMoving !== false;
20545 },
20546 movableColumnBuilder: function (colDef, col, gridOptions) {
20547 var promises = [];
20548 /**
20549 * @ngdoc object
20550 * @name ui.grid.moveColumns.api:ColumnDef
20551 *
20552 * @description Column Definition for move column feature, these are available to be
20553 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
20554 */
20555 /**
20556 * @ngdoc object
20557 * @name enableColumnMoving
20558 * @propertyOf ui.grid.moveColumns.api:ColumnDef
20559 * @description Enable column moving for the column.
20560 */
20561 colDef.enableColumnMoving = colDef.enableColumnMoving === undefined ? gridOptions.enableColumnMoving
20562 : colDef.enableColumnMoving;
20563 return $q.all(promises);
20564 },
20565 /**
20566 * @ngdoc method
20567 * @name updateColumnCache
20568 * @methodOf ui.grid.moveColumns
20569 * @description Cache the current order of columns, so we can restore them after new columnDefs are defined
20570 */
20571 updateColumnCache: function(grid){
20572 grid.moveColumns.orderCache = grid.getOnlyDataColumns();
20573 },
20574 /**
20575 * @ngdoc method
20576 * @name verifyColumnOrder
20577 * @methodOf ui.grid.moveColumns
20578 * @description dataChangeCallback which uses the cached column order to restore the column order
20579 * when it is reset by altering the columnDefs array.
20580 */
20581 verifyColumnOrder: function(grid){
20582 var headerRowOffset = grid.rowHeaderColumns.length;
20583 var newIndex;
20584
20585 angular.forEach(grid.moveColumns.orderCache, function(cacheCol, cacheIndex){
20586 newIndex = grid.columns.indexOf(cacheCol);
20587 if ( newIndex !== -1 && newIndex - headerRowOffset !== cacheIndex ){
20588 var column = grid.columns.splice(newIndex, 1)[0];
20589 grid.columns.splice(cacheIndex + headerRowOffset, 0, column);
20590 }
20591 });
20592 },
20593 redrawColumnAtPosition: function (grid, originalPosition, newPosition) {
20594
20595 var columns = grid.columns;
20596
20597 var originalColumn = columns[originalPosition];
20598 if (originalColumn.colDef.enableColumnMoving) {
20599 if (originalPosition > newPosition) {
20600 for (var i1 = originalPosition; i1 > newPosition; i1--) {
20601 columns[i1] = columns[i1 - 1];
20602 }
20603 }
20604 else if (newPosition > originalPosition) {
20605 for (var i2 = originalPosition; i2 < newPosition; i2++) {
20606 columns[i2] = columns[i2 + 1];
20607 }
20608 }
20609 columns[newPosition] = originalColumn;
20610 service.updateColumnCache(grid);
20611 grid.queueGridRefresh();
20612 $timeout(function () {
20613 grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN );
20614 grid.api.colMovable.raise.columnPositionChanged(originalColumn.colDef, originalPosition, newPosition);
20615 });
20616 }
20617 }
20618 };
20619 return service;
20620 }]);
20621
20622 /**
20623 * @ngdoc directive
20624 * @name ui.grid.moveColumns.directive:uiGridMoveColumns
20625 * @element div
20626 * @restrict A
20627 * @description Adds column moving features to the ui-grid directive.
20628 * @example
20629 <example module="app">
20630 <file name="app.js">
20631 var app = angular.module('app', ['ui.grid', 'ui.grid.moveColumns']);
20632 app.controller('MainCtrl', ['$scope', function ($scope) {
20633 $scope.data = [
20634 { name: 'Bob', title: 'CEO', age: 45 },
20635 { name: 'Frank', title: 'Lowly Developer', age: 25 },
20636 { name: 'Jenny', title: 'Highly Developer', age: 35 }
20637 ];
20638 $scope.columnDefs = [
20639 {name: 'name'},
20640 {name: 'title'},
20641 {name: 'age'}
20642 ];
20643 }]);
20644 </file>
20645 <file name="main.css">
20646 .grid {
20647 width: 100%;
20648 height: 150px;
20649 }
20650 </file>
20651 <file name="index.html">
20652 <div ng-controller="MainCtrl">
20653 <div class="grid" ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-move-columns></div>
20654 </div>
20655 </file>
20656 </example>
20657 */
20658 module.directive('uiGridMoveColumns', ['uiGridMoveColumnService', function (uiGridMoveColumnService) {
20659 return {
20660 replace: true,
20661 priority: 0,
20662 require: '^uiGrid',
20663 scope: false,
20664 compile: function () {
20665 return {
20666 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
20667 uiGridMoveColumnService.initializeGrid(uiGridCtrl.grid);
20668 },
20669 post: function ($scope, $elm, $attrs, uiGridCtrl) {
20670 }
20671 };
20672 }
20673 };
20674 }]);
20675
20676 /**
20677 * @ngdoc directive
20678 * @name ui.grid.moveColumns.directive:uiGridHeaderCell
20679 * @element div
20680 * @restrict A
20681 *
20682 * @description Stacks on top of ui.grid.uiGridHeaderCell to provide capability to be able to move it to reposition column.
20683 *
20684 * On receiving mouseDown event headerCell is cloned, now as the mouse moves the cloned header cell also moved in the grid.
20685 * In case the moving cloned header cell reaches the left or right extreme of grid, grid scrolling is triggered (if horizontal scroll exists).
20686 * On mouseUp event column is repositioned at position where mouse is released and cloned header cell is removed.
20687 *
20688 * Events that invoke cloning of header cell:
20689 * - mousedown
20690 *
20691 * Events that invoke movement of cloned header cell:
20692 * - mousemove
20693 *
20694 * Events that invoke repositioning of column:
20695 * - mouseup
20696 */
20697 module.directive('uiGridHeaderCell', ['$q', 'gridUtil', 'uiGridMoveColumnService', '$document', '$log', 'uiGridConstants', 'ScrollEvent',
20698 function ($q, gridUtil, uiGridMoveColumnService, $document, $log, uiGridConstants, ScrollEvent) {
20699 return {
20700 priority: -10,
20701 require: '^uiGrid',
20702 compile: function () {
20703 return {
20704 post: function ($scope, $elm, $attrs, uiGridCtrl) {
20705
20706 if ($scope.col.colDef.enableColumnMoving) {
20707
20708 /*
20709 * Our general approach to column move is that we listen to a touchstart or mousedown
20710 * event over the column header. When we hear one, then we wait for a move of the same type
20711 * - if we are a touchstart then we listen for a touchmove, if we are a mousedown we listen for
20712 * a mousemove (i.e. a drag) before we decide that there's a move underway. If there's never a move,
20713 * and we instead get a mouseup or a touchend, then we just drop out again and do nothing.
20714 *
20715 */
20716 var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') );
20717
20718 var gridLeft;
20719 var previousMouseX;
20720 var totalMouseMovement;
20721 var rightMoveLimit;
20722 var elmCloned = false;
20723 var movingElm;
20724 var reducedWidth;
20725 var moveOccurred = false;
20726
20727 var downFn = function( event ){
20728 //Setting some variables required for calculations.
20729 gridLeft = $scope.grid.element[0].getBoundingClientRect().left;
20730 if ( $scope.grid.hasLeftContainer() ){
20731 gridLeft += $scope.grid.renderContainers.left.header[0].getBoundingClientRect().width;
20732 }
20733
20734 previousMouseX = event.pageX;
20735 totalMouseMovement = 0;
20736 rightMoveLimit = gridLeft + $scope.grid.getViewportWidth();
20737
20738 if ( event.type === 'mousedown' ){
20739 $document.on('mousemove', moveFn);
20740 $document.on('mouseup', upFn);
20741 } else if ( event.type === 'touchstart' ){
20742 $document.on('touchmove', moveFn);
20743 $document.on('touchend', upFn);
20744 }
20745 };
20746
20747 var moveFn = function( event ) {
20748 var changeValue = event.pageX - previousMouseX;
20749 if ( changeValue === 0 ){ return; }
20750 //Disable text selection in Chrome during column move
20751 document.onselectstart = function() { return false; };
20752
20753 moveOccurred = true;
20754
20755 if (!elmCloned) {
20756 cloneElement();
20757 }
20758 else if (elmCloned) {
20759 moveElement(changeValue);
20760 previousMouseX = event.pageX;
20761 }
20762 };
20763
20764 var upFn = function( event ){
20765 //Re-enable text selection after column move
20766 document.onselectstart = null;
20767
20768 //Remove the cloned element on mouse up.
20769 if (movingElm) {
20770 movingElm.remove();
20771 elmCloned = false;
20772 }
20773
20774 offAllEvents();
20775 onDownEvents();
20776
20777 if (!moveOccurred){
20778 return;
20779 }
20780
20781 var columns = $scope.grid.columns;
20782 var columnIndex = 0;
20783 for (var i = 0; i < columns.length; i++) {
20784 if (columns[i].colDef.name !== $scope.col.colDef.name) {
20785 columnIndex++;
20786 }
20787 else {
20788 break;
20789 }
20790 }
20791
20792 //Case where column should be moved to a position on its left
20793 if (totalMouseMovement < 0) {
20794 var totalColumnsLeftWidth = 0;
20795 for (var il = columnIndex - 1; il >= 0; il--) {
20796 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
20797 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
20798 if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) {
20799 uiGridMoveColumnService.redrawColumnAtPosition
20800 ($scope.grid, columnIndex, il + 1);
20801 break;
20802 }
20803 }
20804 }
20805 //Case where column should be moved to beginning of the grid.
20806 if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) {
20807 uiGridMoveColumnService.redrawColumnAtPosition
20808 ($scope.grid, columnIndex, 0);
20809 }
20810 }
20811
20812 //Case where column should be moved to a position on its right
20813 else if (totalMouseMovement > 0) {
20814 var totalColumnsRightWidth = 0;
20815 for (var ir = columnIndex + 1; ir < columns.length; ir++) {
20816 if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) {
20817 totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width;
20818 if (totalColumnsRightWidth > totalMouseMovement) {
20819 uiGridMoveColumnService.redrawColumnAtPosition
20820 ($scope.grid, columnIndex, ir - 1);
20821 break;
20822 }
20823 }
20824 }
20825 //Case where column should be moved to end of the grid.
20826 if (totalColumnsRightWidth < totalMouseMovement) {
20827 uiGridMoveColumnService.redrawColumnAtPosition
20828 ($scope.grid, columnIndex, columns.length - 1);
20829 }
20830 }
20831 };
20832
20833 var onDownEvents = function(){
20834 $contentsElm.on('touchstart', downFn);
20835 $contentsElm.on('mousedown', downFn);
20836 };
20837
20838 var offAllEvents = function() {
20839 $contentsElm.off('touchstart', downFn);
20840 $contentsElm.off('mousedown', downFn);
20841
20842 $document.off('mousemove', moveFn);
20843 $document.off('touchmove', moveFn);
20844
20845 $document.off('mouseup', upFn);
20846 $document.off('touchend', upFn);
20847 };
20848
20849 onDownEvents();
20850
20851
20852 var cloneElement = function () {
20853 elmCloned = true;
20854
20855 //Cloning header cell and appending to current header cell.
20856 movingElm = $elm.clone();
20857 $elm.parent().append(movingElm);
20858
20859 //Left of cloned element should be aligned to original header cell.
20860 movingElm.addClass('movingColumn');
20861 var movingElementStyles = {};
20862 var elmLeft;
20863 if (gridUtil.detectBrowser() === 'safari') {
20864 //Correction for Safari getBoundingClientRect,
20865 //which does not correctly compute when there is an horizontal scroll
20866 elmLeft = $elm[0].offsetLeft + $elm[0].offsetWidth - $elm[0].getBoundingClientRect().width;
20867 }
20868 else {
20869 elmLeft = $elm[0].getBoundingClientRect().left;
20870 }
20871 movingElementStyles.left = (elmLeft - gridLeft) + 'px';
20872 var gridRight = $scope.grid.element[0].getBoundingClientRect().right;
20873 var elmRight = $elm[0].getBoundingClientRect().right;
20874 if (elmRight > gridRight) {
20875 reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight);
20876 movingElementStyles.width = reducedWidth + 'px';
20877 }
20878 movingElm.css(movingElementStyles);
20879 };
20880
20881 var moveElement = function (changeValue) {
20882 //Calculate total column width
20883 var columns = $scope.grid.columns;
20884 var totalColumnWidth = 0;
20885 for (var i = 0; i < columns.length; i++) {
20886 if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) {
20887 totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width;
20888 }
20889 }
20890
20891 //Calculate new position of left of column
20892 var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1;
20893 var currentElmRight = movingElm[0].getBoundingClientRect().right;
20894 var newElementLeft;
20895
20896 newElementLeft = currentElmLeft - gridLeft + changeValue;
20897 newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit;
20898
20899 //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid
20900 if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) {
20901 movingElm.css({visibility: 'visible', 'left': newElementLeft + 'px'});
20902 }
20903 else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) {
20904 changeValue *= 8;
20905 var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement');
20906 scrollEvent.x = {pixels: changeValue};
20907 scrollEvent.grid.scrollContainers('',scrollEvent);
20908 }
20909
20910 //Calculate total width of columns on the left of the moving column and the mouse movement
20911 var totalColumnsLeftWidth = 0;
20912 for (var il = 0; il < columns.length; il++) {
20913 if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) {
20914 if (columns[il].colDef.name !== $scope.col.colDef.name) {
20915 totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width;
20916 }
20917 else {
20918 break;
20919 }
20920 }
20921 }
20922 if ($scope.newScrollLeft === undefined) {
20923 totalMouseMovement += changeValue;
20924 }
20925 else {
20926 totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth;
20927 }
20928
20929 //Increase width of moving column, in case the rightmost column was moved and its width was
20930 //decreased because of overflow
20931 if (reducedWidth < $scope.col.drawnWidth) {
20932 reducedWidth += Math.abs(changeValue);
20933 movingElm.css({'width': reducedWidth + 'px'});
20934 }
20935 };
20936 }
20937 }
20938 };
20939 }
20940 };
20941 }]);
20942 })();
20943
20944 (function() {
20945 'use strict';
20946
20947 /**
20948 * @ngdoc overview
20949 * @name ui.grid.pagination
20950 *
20951 * @description
20952 *
20953 * # ui.grid.pagination
20954 *
20955 * <div class="alert alert-warning" role="alert"><strong>Alpha</strong> This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.</div>
20956 *
20957 * This module provides pagination support to ui-grid
20958 */
20959 var module = angular.module('ui.grid.pagination', ['ng', 'ui.grid']);
20960
20961 /**
20962 * @ngdoc service
20963 * @name ui.grid.pagination.service:uiGridPaginationService
20964 *
20965 * @description Service for the pagination feature
20966 */
20967 module.service('uiGridPaginationService', ['gridUtil',
20968 function (gridUtil) {
20969 var service = {
20970 /**
20971 * @ngdoc method
20972 * @name initializeGrid
20973 * @methodOf ui.grid.pagination.service:uiGridPaginationService
20974 * @description Attaches the service to a certain grid
20975 * @param {Grid} grid The grid we want to work with
20976 */
20977 initializeGrid: function (grid) {
20978 service.defaultGridOptions(grid.options);
20979
20980 /**
20981 * @ngdoc object
20982 * @name ui.grid.pagination.api:PublicAPI
20983 *
20984 * @description Public API for the pagination feature
20985 */
20986 var publicApi = {
20987 events: {
20988 pagination: {
20989 /**
20990 * @ngdoc event
20991 * @name paginationChanged
20992 * @eventOf ui.grid.pagination.api:PublicAPI
20993 * @description This event fires when the pageSize or currentPage changes
20994 * @param {int} currentPage requested page number
20995 * @param {int} pageSize requested page size
20996 */
20997 paginationChanged: function (currentPage, pageSize) { }
20998 }
20999 },
21000 methods: {
21001 pagination: {
21002 /**
21003 * @ngdoc method
21004 * @name getPage
21005 * @methodOf ui.grid.pagination.api:PublicAPI
21006 * @description Returns the number of the current page
21007 */
21008 getPage: function () {
21009 return grid.options.enablePagination ? grid.options.paginationCurrentPage : null;
21010 },
21011 /**
21012 * @ngdoc method
21013 * @name getTotalPages
21014 * @methodOf ui.grid.pagination.api:PublicAPI
21015 * @description Returns the total number of pages
21016 */
21017 getTotalPages: function () {
21018 if (!grid.options.enablePagination) {
21019 return null;
21020 }
21021
21022 return (grid.options.totalItems === 0) ? 1 : Math.ceil(grid.options.totalItems / grid.options.paginationPageSize);
21023 },
21024 /**
21025 * @ngdoc method
21026 * @name nextPage
21027 * @methodOf ui.grid.pagination.api:PublicAPI
21028 * @description Moves to the next page, if possible
21029 */
21030 nextPage: function () {
21031 if (!grid.options.enablePagination) {
21032 return;
21033 }
21034
21035 if (grid.options.totalItems > 0) {
21036 grid.options.paginationCurrentPage = Math.min(
21037 grid.options.paginationCurrentPage + 1,
21038 publicApi.methods.pagination.getTotalPages()
21039 );
21040 } else {
21041 grid.options.paginationCurrentPage++;
21042 }
21043 },
21044 /**
21045 * @ngdoc method
21046 * @name previousPage
21047 * @methodOf ui.grid.pagination.api:PublicAPI
21048 * @description Moves to the previous page, if we're not on the first page
21049 */
21050 previousPage: function () {
21051 if (!grid.options.enablePagination) {
21052 return;
21053 }
21054
21055 grid.options.paginationCurrentPage = Math.max(grid.options.paginationCurrentPage - 1, 1);
21056 },
21057 /**
21058 * @ngdoc method
21059 * @name seek
21060 * @methodOf ui.grid.pagination.api:PublicAPI
21061 * @description Moves to the requested page
21062 * @param {int} page The number of the page that should be displayed
21063 */
21064 seek: function (page) {
21065 if (!grid.options.enablePagination) {
21066 return;
21067 }
21068 if (!angular.isNumber(page) || page < 1) {
21069 throw 'Invalid page number: ' + page;
21070 }
21071
21072 grid.options.paginationCurrentPage = Math.min(page, publicApi.methods.pagination.getTotalPages());
21073 }
21074 }
21075 }
21076 };
21077
21078 grid.api.registerEventsFromObject(publicApi.events);
21079 grid.api.registerMethodsFromObject(publicApi.methods);
21080
21081 var processPagination = function( renderableRows ){
21082 if (grid.options.useExternalPagination || !grid.options.enablePagination) {
21083 return renderableRows;
21084 }
21085 //client side pagination
21086 var pageSize = parseInt(grid.options.paginationPageSize, 10);
21087 var currentPage = parseInt(grid.options.paginationCurrentPage, 10);
21088
21089 var visibleRows = renderableRows.filter(function (row) { return row.visible; });
21090 grid.options.totalItems = visibleRows.length;
21091
21092 var firstRow = (currentPage - 1) * pageSize;
21093 if (firstRow > visibleRows.length) {
21094 currentPage = grid.options.paginationCurrentPage = 1;
21095 firstRow = (currentPage - 1) * pageSize;
21096 }
21097 return visibleRows.slice(firstRow, firstRow + pageSize);
21098 };
21099
21100 grid.registerRowsProcessor(processPagination, 900 );
21101
21102 },
21103 defaultGridOptions: function (gridOptions) {
21104 /**
21105 * @ngdoc object
21106 * @name ui.grid.pagination.api:GridOptions
21107 *
21108 * @description GridOptions for the pagination feature, these are available to be
21109 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21110 */
21111
21112 /**
21113 * @ngdoc property
21114 * @name enablePagination
21115 * @propertyOf ui.grid.pagination.api:GridOptions
21116 * @description Enables pagination, defaults to true
21117 */
21118 gridOptions.enablePagination = gridOptions.enablePagination !== false;
21119 /**
21120 * @ngdoc property
21121 * @name enablePaginationControls
21122 * @propertyOf ui.grid.pagination.api:GridOptions
21123 * @description Enables the paginator at the bottom of the grid. Turn this off, if you want to implement your
21124 * own controls outside the grid.
21125 */
21126 gridOptions.enablePaginationControls = gridOptions.enablePaginationControls !== false;
21127 /**
21128 * @ngdoc property
21129 * @name useExternalPagination
21130 * @propertyOf ui.grid.pagination.api:GridOptions
21131 * @description Disables client side pagination. When true, handle the paginationChanged event and set data
21132 * and totalItems, defaults to `false`
21133 */
21134 gridOptions.useExternalPagination = gridOptions.useExternalPagination === true;
21135 /**
21136 * @ngdoc property
21137 * @name totalItems
21138 * @propertyOf ui.grid.pagination.api:GridOptions
21139 * @description Total number of items, set automatically when client side pagination, needs set by user
21140 * for server side pagination
21141 */
21142 if (gridUtil.isNullOrUndefined(gridOptions.totalItems)) {
21143 gridOptions.totalItems = 0;
21144 }
21145 /**
21146 * @ngdoc property
21147 * @name paginationPageSizes
21148 * @propertyOf ui.grid.pagination.api:GridOptions
21149 * @description Array of page sizes, defaults to `[250, 500, 1000]`
21150 */
21151 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSizes)) {
21152 gridOptions.paginationPageSizes = [250, 500, 1000];
21153 }
21154 /**
21155 * @ngdoc property
21156 * @name paginationPageSize
21157 * @propertyOf ui.grid.pagination.api:GridOptions
21158 * @description Page size, defaults to the first item in paginationPageSizes, or 0 if paginationPageSizes is empty
21159 */
21160 if (gridUtil.isNullOrUndefined(gridOptions.paginationPageSize)) {
21161 if (gridOptions.paginationPageSizes.length > 0) {
21162 gridOptions.paginationPageSize = gridOptions.paginationPageSizes[0];
21163 } else {
21164 gridOptions.paginationPageSize = 0;
21165 }
21166 }
21167 /**
21168 * @ngdoc property
21169 * @name paginationCurrentPage
21170 * @propertyOf ui.grid.pagination.api:GridOptions
21171 * @description Current page number, defaults to 1
21172 */
21173 if (gridUtil.isNullOrUndefined(gridOptions.paginationCurrentPage)) {
21174 gridOptions.paginationCurrentPage = 1;
21175 }
21176
21177 /**
21178 * @ngdoc property
21179 * @name paginationTemplate
21180 * @propertyOf ui.grid.pagination.api:GridOptions
21181 * @description A custom template for the pager, defaults to `ui-grid/pagination`
21182 */
21183 if (gridUtil.isNullOrUndefined(gridOptions.paginationTemplate)) {
21184 gridOptions.paginationTemplate = 'ui-grid/pagination';
21185 }
21186 },
21187 /**
21188 * @ngdoc method
21189 * @methodOf ui.grid.pagination.service:uiGridPaginationService
21190 * @name uiGridPaginationService
21191 * @description Raises paginationChanged and calls refresh for client side pagination
21192 * @param {Grid} grid the grid for which the pagination changed
21193 * @param {int} currentPage requested page number
21194 * @param {int} pageSize requested page size
21195 */
21196 onPaginationChanged: function (grid, currentPage, pageSize) {
21197 grid.api.pagination.raise.paginationChanged(currentPage, pageSize);
21198 if (!grid.options.useExternalPagination) {
21199 grid.queueGridRefresh(); //client side pagination
21200 }
21201 }
21202 };
21203
21204 return service;
21205 }
21206 ]);
21207 /**
21208 * @ngdoc directive
21209 * @name ui.grid.pagination.directive:uiGridPagination
21210 * @element div
21211 * @restrict A
21212 *
21213 * @description Adds pagination features to grid
21214 * @example
21215 <example module="app">
21216 <file name="app.js">
21217 var app = angular.module('app', ['ui.grid', 'ui.grid.pagination']);
21218
21219 app.controller('MainCtrl', ['$scope', function ($scope) {
21220 $scope.data = [
21221 { name: 'Alex', car: 'Toyota' },
21222 { name: 'Sam', car: 'Lexus' },
21223 { name: 'Joe', car: 'Dodge' },
21224 { name: 'Bob', car: 'Buick' },
21225 { name: 'Cindy', car: 'Ford' },
21226 { name: 'Brian', car: 'Audi' },
21227 { name: 'Malcom', car: 'Mercedes Benz' },
21228 { name: 'Dave', car: 'Ford' },
21229 { name: 'Stacey', car: 'Audi' },
21230 { name: 'Amy', car: 'Acura' },
21231 { name: 'Scott', car: 'Toyota' },
21232 { name: 'Ryan', car: 'BMW' },
21233 ];
21234
21235 $scope.gridOptions = {
21236 data: 'data',
21237 paginationPageSizes: [5, 10, 25],
21238 paginationPageSize: 5,
21239 columnDefs: [
21240 {name: 'name'},
21241 {name: 'car'}
21242 ]
21243 }
21244 }]);
21245 </file>
21246 <file name="index.html">
21247 <div ng-controller="MainCtrl">
21248 <div ui-grid="gridOptions" ui-grid-pagination></div>
21249 </div>
21250 </file>
21251 </example>
21252 */
21253 module.directive('uiGridPagination', ['gridUtil', 'uiGridPaginationService',
21254 function (gridUtil, uiGridPaginationService) {
21255 return {
21256 priority: -200,
21257 scope: false,
21258 require: 'uiGrid',
21259 link: {
21260 pre: function ($scope, $elm, $attr, uiGridCtrl) {
21261 uiGridPaginationService.initializeGrid(uiGridCtrl.grid);
21262
21263 gridUtil.getTemplate(uiGridCtrl.grid.options.paginationTemplate)
21264 .then(function (contents) {
21265 var template = angular.element(contents);
21266 $elm.append(template);
21267 uiGridCtrl.innerCompile(template);
21268 });
21269 }
21270 }
21271 };
21272 }
21273 ]);
21274
21275 /**
21276 * @ngdoc directive
21277 * @name ui.grid.pagination.directive:uiGridPager
21278 * @element div
21279 *
21280 * @description Panel for handling pagination
21281 */
21282 module.directive('uiGridPager', ['uiGridPaginationService', 'uiGridConstants', 'gridUtil', 'i18nService',
21283 function (uiGridPaginationService, uiGridConstants, gridUtil, i18nService) {
21284 return {
21285 priority: -200,
21286 scope: true,
21287 require: '^uiGrid',
21288 link: function ($scope, $elm, $attr, uiGridCtrl) {
21289 var defaultFocusElementSelector = '.ui-grid-pager-control-input';
21290 $scope.aria = i18nService.getSafeText('pagination.aria'); //Returns an object with all of the aria labels
21291
21292 $scope.paginationApi = uiGridCtrl.grid.api.pagination;
21293 $scope.sizesLabel = i18nService.getSafeText('pagination.sizes');
21294 $scope.totalItemsLabel = i18nService.getSafeText('pagination.totalItems');
21295 $scope.paginationOf = i18nService.getSafeText('pagination.of');
21296 $scope.paginationThrough = i18nService.getSafeText('pagination.through');
21297
21298 var options = uiGridCtrl.grid.options;
21299
21300 uiGridCtrl.grid.renderContainers.body.registerViewportAdjuster(function (adjustment) {
21301 adjustment.height = adjustment.height - gridUtil.elementHeight($elm);
21302 return adjustment;
21303 });
21304
21305 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback(function (grid) {
21306 if (!grid.options.useExternalPagination) {
21307 grid.options.totalItems = grid.rows.length;
21308 }
21309 }, [uiGridConstants.dataChange.ROW]);
21310
21311 $scope.$on('$destroy', dataChangeDereg);
21312
21313 var setShowing = function () {
21314 $scope.showingLow = ((options.paginationCurrentPage - 1) * options.paginationPageSize) + 1;
21315 $scope.showingHigh = Math.min(options.paginationCurrentPage * options.paginationPageSize, options.totalItems);
21316 };
21317
21318 var deregT = $scope.$watch('grid.options.totalItems + grid.options.paginationPageSize', setShowing);
21319
21320 var deregP = $scope.$watch('grid.options.paginationCurrentPage + grid.options.paginationPageSize', function (newValues, oldValues) {
21321 if (newValues === oldValues || oldValues === undefined) {
21322 return;
21323 }
21324
21325 if (!angular.isNumber(options.paginationCurrentPage) || options.paginationCurrentPage < 1) {
21326 options.paginationCurrentPage = 1;
21327 return;
21328 }
21329
21330 if (options.totalItems > 0 && options.paginationCurrentPage > $scope.paginationApi.getTotalPages()) {
21331 options.paginationCurrentPage = $scope.paginationApi.getTotalPages();
21332 return;
21333 }
21334
21335 setShowing();
21336 uiGridPaginationService.onPaginationChanged($scope.grid, options.paginationCurrentPage, options.paginationPageSize);
21337 }
21338 );
21339
21340 $scope.$on('$destroy', function() {
21341 deregT();
21342 deregP();
21343 });
21344
21345 $scope.cantPageForward = function () {
21346 if (options.totalItems > 0) {
21347 return options.paginationCurrentPage >= $scope.paginationApi.getTotalPages();
21348 } else {
21349 return options.data.length < 1;
21350 }
21351 };
21352
21353 $scope.cantPageToLast = function () {
21354 if (options.totalItems > 0) {
21355 return $scope.cantPageForward();
21356 } else {
21357 return true;
21358 }
21359 };
21360
21361 $scope.cantPageBackward = function () {
21362 return options.paginationCurrentPage <= 1;
21363 };
21364
21365 var focusToInputIf = function(condition){
21366 if (condition){
21367 gridUtil.focus.bySelector($elm, defaultFocusElementSelector);
21368 }
21369 };
21370
21371 //Takes care of setting focus to the middle element when focus is lost
21372 $scope.pageFirstPageClick = function () {
21373 $scope.paginationApi.seek(1);
21374 focusToInputIf($scope.cantPageBackward());
21375 };
21376
21377 $scope.pagePreviousPageClick = function () {
21378 $scope.paginationApi.previousPage();
21379 focusToInputIf($scope.cantPageBackward());
21380 };
21381
21382 $scope.pageNextPageClick = function () {
21383 $scope.paginationApi.nextPage();
21384 focusToInputIf($scope.cantPageForward());
21385 };
21386
21387 $scope.pageLastPageClick = function () {
21388 $scope.paginationApi.seek($scope.paginationApi.getTotalPages());
21389 focusToInputIf($scope.cantPageToLast());
21390 };
21391
21392 }
21393 };
21394 }
21395 ]);
21396 })();
21397
21398 (function () {
21399 'use strict';
21400
21401 /**
21402 * @ngdoc overview
21403 * @name ui.grid.pinning
21404 * @description
21405 *
21406 * # ui.grid.pinning
21407 *
21408 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
21409 *
21410 * This module provides column pinning to the end user via menu options in the column header
21411 *
21412 * <div doc-module-components="ui.grid.pinning"></div>
21413 */
21414
21415 var module = angular.module('ui.grid.pinning', ['ui.grid']);
21416
21417 module.constant('uiGridPinningConstants', {
21418 container: {
21419 LEFT: 'left',
21420 RIGHT: 'right',
21421 NONE: ''
21422 }
21423 });
21424
21425 module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', 'uiGridPinningConstants', function (gridUtil, GridRenderContainer, i18nService, uiGridPinningConstants) {
21426 var service = {
21427
21428 initializeGrid: function (grid) {
21429 service.defaultGridOptions(grid.options);
21430
21431 // Register a column builder to add new menu items for pinning left and right
21432 grid.registerColumnBuilder(service.pinningColumnBuilder);
21433
21434 /**
21435 * @ngdoc object
21436 * @name ui.grid.pinning.api:PublicApi
21437 *
21438 * @description Public Api for pinning feature
21439 */
21440 var publicApi = {
21441 events: {
21442 pinning: {
21443 /**
21444 * @ngdoc event
21445 * @name columnPin
21446 * @eventOf ui.grid.pinning.api:PublicApi
21447 * @description raised when column pin state has changed
21448 * <pre>
21449 * gridApi.pinning.on.columnPinned(scope, function(colDef){})
21450 * </pre>
21451 * @param {object} colDef the column that was changed
21452 * @param {string} container the render container the column is in ('left', 'right', '')
21453 */
21454 columnPinned: function(colDef, container) {
21455 }
21456 }
21457 },
21458 methods: {
21459 pinning: {
21460 /**
21461 * @ngdoc function
21462 * @name pinColumn
21463 * @methodOf ui.grid.pinning.api:PublicApi
21464 * @description pin column left, right, or none
21465 * <pre>
21466 * gridApi.pinning.pinColumn(col, uiGridPinningConstants.container.LEFT)
21467 * </pre>
21468 * @param {gridColumn} col the column being pinned
21469 * @param {string} container one of the recognised types
21470 * from uiGridPinningConstants
21471 */
21472 pinColumn: function(col, container) {
21473 service.pinColumn(grid, col, container);
21474 }
21475 }
21476 }
21477 };
21478
21479 grid.api.registerEventsFromObject(publicApi.events);
21480 grid.api.registerMethodsFromObject(publicApi.methods);
21481 },
21482
21483 defaultGridOptions: function (gridOptions) {
21484 //default option to true unless it was explicitly set to false
21485 /**
21486 * @ngdoc object
21487 * @name ui.grid.pinning.api:GridOptions
21488 *
21489 * @description GridOptions for pinning feature, these are available to be
21490 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21491 */
21492
21493 /**
21494 * @ngdoc object
21495 * @name enablePinning
21496 * @propertyOf ui.grid.pinning.api:GridOptions
21497 * @description Enable pinning for the entire grid.
21498 * <br/>Defaults to true
21499 */
21500 gridOptions.enablePinning = gridOptions.enablePinning !== false;
21501
21502 },
21503
21504 pinningColumnBuilder: function (colDef, col, gridOptions) {
21505 //default to true unless gridOptions or colDef is explicitly false
21506
21507 /**
21508 * @ngdoc object
21509 * @name ui.grid.pinning.api:ColumnDef
21510 *
21511 * @description ColumnDef for pinning feature, these are available to be
21512 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
21513 */
21514
21515 /**
21516 * @ngdoc object
21517 * @name enablePinning
21518 * @propertyOf ui.grid.pinning.api:ColumnDef
21519 * @description Enable pinning for the individual column.
21520 * <br/>Defaults to true
21521 */
21522 colDef.enablePinning = colDef.enablePinning === undefined ? gridOptions.enablePinning : colDef.enablePinning;
21523
21524
21525 /**
21526 * @ngdoc object
21527 * @name pinnedLeft
21528 * @propertyOf ui.grid.pinning.api:ColumnDef
21529 * @description Column is pinned left when grid is rendered
21530 * <br/>Defaults to false
21531 */
21532
21533 /**
21534 * @ngdoc object
21535 * @name pinnedRight
21536 * @propertyOf ui.grid.pinning.api:ColumnDef
21537 * @description Column is pinned right when grid is rendered
21538 * <br/>Defaults to false
21539 */
21540 if (colDef.pinnedLeft) {
21541 col.renderContainer = 'left';
21542 col.grid.createLeftContainer();
21543 }
21544 else if (colDef.pinnedRight) {
21545 col.renderContainer = 'right';
21546 col.grid.createRightContainer();
21547 }
21548
21549 if (!colDef.enablePinning) {
21550 return;
21551 }
21552
21553 var pinColumnLeftAction = {
21554 name: 'ui.grid.pinning.pinLeft',
21555 title: i18nService.get().pinning.pinLeft,
21556 icon: 'ui-grid-icon-left-open',
21557 shown: function () {
21558 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'left';
21559 },
21560 action: function () {
21561 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.LEFT);
21562 }
21563 };
21564
21565 var pinColumnRightAction = {
21566 name: 'ui.grid.pinning.pinRight',
21567 title: i18nService.get().pinning.pinRight,
21568 icon: 'ui-grid-icon-right-open',
21569 shown: function () {
21570 return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'right';
21571 },
21572 action: function () {
21573 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.RIGHT);
21574 }
21575 };
21576
21577 var removePinAction = {
21578 name: 'ui.grid.pinning.unpin',
21579 title: i18nService.get().pinning.unpin,
21580 icon: 'ui-grid-icon-cancel',
21581 shown: function () {
21582 return typeof(this.context.col.renderContainer) !== 'undefined' && this.context.col.renderContainer !== null && this.context.col.renderContainer !== 'body';
21583 },
21584 action: function () {
21585 service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.UNPIN);
21586 }
21587 };
21588
21589 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinLeft')) {
21590 col.menuItems.push(pinColumnLeftAction);
21591 }
21592 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.pinRight')) {
21593 col.menuItems.push(pinColumnRightAction);
21594 }
21595 if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.unpin')) {
21596 col.menuItems.push(removePinAction);
21597 }
21598 },
21599
21600 pinColumn: function(grid, col, container) {
21601 if (container === uiGridPinningConstants.container.NONE) {
21602 col.renderContainer = null;
21603 }
21604 else {
21605 col.renderContainer = container;
21606 if (container === uiGridPinningConstants.container.LEFT) {
21607 grid.createLeftContainer();
21608 }
21609 else if (container === uiGridPinningConstants.container.RIGHT) {
21610 grid.createRightContainer();
21611 }
21612 }
21613
21614 grid.refresh()
21615 .then(function() {
21616 grid.api.pinning.raise.columnPinned( col.colDef, container );
21617 });
21618 }
21619 };
21620
21621 return service;
21622 }]);
21623
21624 module.directive('uiGridPinning', ['gridUtil', 'uiGridPinningService',
21625 function (gridUtil, uiGridPinningService) {
21626 return {
21627 require: 'uiGrid',
21628 scope: false,
21629 compile: function () {
21630 return {
21631 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
21632 uiGridPinningService.initializeGrid(uiGridCtrl.grid);
21633 },
21634 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21635 }
21636 };
21637 }
21638 };
21639 }]);
21640
21641
21642 })();
21643
21644 (function(){
21645 'use strict';
21646
21647 /**
21648 * @ngdoc overview
21649 * @name ui.grid.resizeColumns
21650 * @description
21651 *
21652 * # ui.grid.resizeColumns
21653 *
21654 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
21655 *
21656 * This module allows columns to be resized.
21657 */
21658 var module = angular.module('ui.grid.resizeColumns', ['ui.grid']);
21659
21660 module.service('uiGridResizeColumnsService', ['gridUtil', '$q', '$timeout',
21661 function (gridUtil, $q, $timeout) {
21662
21663 var service = {
21664 defaultGridOptions: function(gridOptions){
21665 //default option to true unless it was explicitly set to false
21666 /**
21667 * @ngdoc object
21668 * @name ui.grid.resizeColumns.api:GridOptions
21669 *
21670 * @description GridOptions for resizeColumns feature, these are available to be
21671 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
21672 */
21673
21674 /**
21675 * @ngdoc object
21676 * @name enableColumnResizing
21677 * @propertyOf ui.grid.resizeColumns.api:GridOptions
21678 * @description Enable column resizing on the entire grid
21679 * <br/>Defaults to true
21680 */
21681 gridOptions.enableColumnResizing = gridOptions.enableColumnResizing !== false;
21682
21683 //legacy support
21684 //use old name if it is explicitly false
21685 if (gridOptions.enableColumnResize === false){
21686 gridOptions.enableColumnResizing = false;
21687 }
21688 },
21689
21690 colResizerColumnBuilder: function (colDef, col, gridOptions) {
21691
21692 var promises = [];
21693 /**
21694 * @ngdoc object
21695 * @name ui.grid.resizeColumns.api:ColumnDef
21696 *
21697 * @description ColumnDef for resizeColumns feature, these are available to be
21698 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
21699 */
21700
21701 /**
21702 * @ngdoc object
21703 * @name enableColumnResizing
21704 * @propertyOf ui.grid.resizeColumns.api:ColumnDef
21705 * @description Enable column resizing on an individual column
21706 * <br/>Defaults to GridOptions.enableColumnResizing
21707 */
21708 //default to true unless gridOptions or colDef is explicitly false
21709 colDef.enableColumnResizing = colDef.enableColumnResizing === undefined ? gridOptions.enableColumnResizing : colDef.enableColumnResizing;
21710
21711
21712 //legacy support of old option name
21713 if (colDef.enableColumnResize === false){
21714 colDef.enableColumnResizing = false;
21715 }
21716
21717 return $q.all(promises);
21718 },
21719
21720 registerPublicApi: function (grid) {
21721 /**
21722 * @ngdoc object
21723 * @name ui.grid.resizeColumns.api:PublicApi
21724 * @description Public Api for column resize feature.
21725 */
21726 var publicApi = {
21727 events: {
21728 /**
21729 * @ngdoc event
21730 * @name columnSizeChanged
21731 * @eventOf ui.grid.resizeColumns.api:PublicApi
21732 * @description raised when column is resized
21733 * <pre>
21734 * gridApi.colResizable.on.columnSizeChanged(scope,function(colDef, deltaChange){})
21735 * </pre>
21736 * @param {object} colDef the column that was resized
21737 * @param {integer} delta of the column size change
21738 */
21739 colResizable: {
21740 columnSizeChanged: function (colDef, deltaChange) {
21741 }
21742 }
21743 }
21744 };
21745 grid.api.registerEventsFromObject(publicApi.events);
21746 },
21747
21748 fireColumnSizeChanged: function (grid, colDef, deltaChange) {
21749 $timeout(function () {
21750 if ( grid.api.colResizable ){
21751 grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange);
21752 } else {
21753 gridUtil.logError("The resizeable api is not registered, this may indicate that you've included the module but not added the 'ui-grid-resize-columns' directive to your grid definition. Cannot raise any events.");
21754 }
21755 });
21756 },
21757
21758 // get either this column, or the column next to this column, to resize,
21759 // returns the column we're going to resize
21760 findTargetCol: function(col, position, rtlMultiplier){
21761 var renderContainer = col.getRenderContainer();
21762
21763 if (position === 'left') {
21764 // Get the column to the left of this one
21765 var colIndex = renderContainer.visibleColumnCache.indexOf(col);
21766 return renderContainer.visibleColumnCache[colIndex - 1 * rtlMultiplier];
21767 } else {
21768 return col;
21769 }
21770 }
21771
21772 };
21773
21774 return service;
21775
21776 }]);
21777
21778
21779 /**
21780 * @ngdoc directive
21781 * @name ui.grid.resizeColumns.directive:uiGridResizeColumns
21782 * @element div
21783 * @restrict A
21784 * @description
21785 * Enables resizing for all columns on the grid. If, for some reason, you want to use the ui-grid-resize-columns directive, but not allow column resizing, you can explicitly set the
21786 * option to false. This prevents resizing for the entire grid, regardless of individual columnDef options.
21787 *
21788 * @example
21789 <doc:example module="app">
21790 <doc:source>
21791 <script>
21792 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
21793
21794 app.controller('MainCtrl', ['$scope', function ($scope) {
21795 $scope.gridOpts = {
21796 data: [
21797 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
21798 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
21799 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
21800 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
21801 ]
21802 };
21803 }]);
21804 </script>
21805
21806 <div ng-controller="MainCtrl">
21807 <div class="testGrid" ui-grid="gridOpts" ui-grid-resize-columns ></div>
21808 </div>
21809 </doc:source>
21810 <doc:scenario>
21811
21812 </doc:scenario>
21813 </doc:example>
21814 */
21815 module.directive('uiGridResizeColumns', ['gridUtil', 'uiGridResizeColumnsService', function (gridUtil, uiGridResizeColumnsService) {
21816 return {
21817 replace: true,
21818 priority: 0,
21819 require: '^uiGrid',
21820 scope: false,
21821 compile: function () {
21822 return {
21823 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
21824 uiGridResizeColumnsService.defaultGridOptions(uiGridCtrl.grid.options);
21825 uiGridCtrl.grid.registerColumnBuilder( uiGridResizeColumnsService.colResizerColumnBuilder);
21826 uiGridResizeColumnsService.registerPublicApi(uiGridCtrl.grid);
21827 },
21828 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21829 }
21830 };
21831 }
21832 };
21833 }]);
21834
21835 // Extend the uiGridHeaderCell directive
21836 module.directive('uiGridHeaderCell', ['gridUtil', '$templateCache', '$compile', '$q', 'uiGridResizeColumnsService', 'uiGridConstants', '$timeout', function (gridUtil, $templateCache, $compile, $q, uiGridResizeColumnsService, uiGridConstants, $timeout) {
21837 return {
21838 // Run after the original uiGridHeaderCell
21839 priority: -10,
21840 require: '^uiGrid',
21841 // scope: false,
21842 compile: function() {
21843 return {
21844 post: function ($scope, $elm, $attrs, uiGridCtrl) {
21845 var grid = uiGridCtrl.grid;
21846
21847 if (grid.options.enableColumnResizing) {
21848 var columnResizerElm = $templateCache.get('ui-grid/columnResizer');
21849
21850 var rtlMultiplier = 1;
21851 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
21852 if (grid.isRTL()) {
21853 $scope.position = 'left';
21854 rtlMultiplier = -1;
21855 }
21856
21857 var displayResizers = function(){
21858
21859 // remove any existing resizers.
21860 var resizers = $elm[0].getElementsByClassName('ui-grid-column-resizer');
21861 for ( var i = 0; i < resizers.length; i++ ){
21862 angular.element(resizers[i]).remove();
21863 }
21864
21865 // get the target column for the left resizer
21866 var otherCol = uiGridResizeColumnsService.findTargetCol($scope.col, 'left', rtlMultiplier);
21867 var renderContainer = $scope.col.getRenderContainer();
21868
21869 // Don't append the left resizer if this is the first column or the column to the left of this one has resizing disabled
21870 if (otherCol && renderContainer.visibleColumnCache.indexOf($scope.col) !== 0 && otherCol.colDef.enableColumnResizing !== false) {
21871 var resizerLeft = angular.element(columnResizerElm).clone();
21872 resizerLeft.attr('position', 'left');
21873
21874 $elm.prepend(resizerLeft);
21875 $compile(resizerLeft)($scope);
21876 }
21877
21878 // Don't append the right resizer if this column has resizing disabled
21879 if ($scope.col.colDef.enableColumnResizing !== false) {
21880 var resizerRight = angular.element(columnResizerElm).clone();
21881 resizerRight.attr('position', 'right');
21882
21883 $elm.append(resizerRight);
21884 $compile(resizerRight)($scope);
21885 }
21886 };
21887
21888 displayResizers();
21889
21890 var waitDisplay = function(){
21891 $timeout(displayResizers);
21892 };
21893
21894 var dataChangeDereg = grid.registerDataChangeCallback( waitDisplay, [uiGridConstants.dataChange.COLUMN] );
21895
21896 $scope.$on( '$destroy', dataChangeDereg );
21897 }
21898 }
21899 };
21900 }
21901 };
21902 }]);
21903
21904
21905
21906 /**
21907 * @ngdoc directive
21908 * @name ui.grid.resizeColumns.directive:uiGridColumnResizer
21909 * @element div
21910 * @restrict A
21911 *
21912 * @description
21913 * Draggable handle that controls column resizing.
21914 *
21915 * @example
21916 <doc:example module="app">
21917 <doc:source>
21918 <script>
21919 var app = angular.module('app', ['ui.grid', 'ui.grid.resizeColumns']);
21920
21921 app.controller('MainCtrl', ['$scope', function ($scope) {
21922 $scope.gridOpts = {
21923 enableColumnResizing: true,
21924 data: [
21925 { "name": "Ethel Price", "gender": "female", "company": "Enersol" },
21926 { "name": "Claudine Neal", "gender": "female", "company": "Sealoud" },
21927 { "name": "Beryl Rice", "gender": "female", "company": "Velity" },
21928 { "name": "Wilder Gonzales", "gender": "male", "company": "Geekko" }
21929 ]
21930 };
21931 }]);
21932 </script>
21933
21934 <div ng-controller="MainCtrl">
21935 <div class="testGrid" ui-grid="gridOpts"></div>
21936 </div>
21937 </doc:source>
21938 <doc:scenario>
21939 // TODO: e2e specs?
21940
21941 // TODO: post-resize a horizontal scroll event should be fired
21942 </doc:scenario>
21943 </doc:example>
21944 */
21945 module.directive('uiGridColumnResizer', ['$document', 'gridUtil', 'uiGridConstants', 'uiGridResizeColumnsService', function ($document, gridUtil, uiGridConstants, uiGridResizeColumnsService) {
21946 var resizeOverlay = angular.element('<div class="ui-grid-resize-overlay"></div>');
21947
21948 var resizer = {
21949 priority: 0,
21950 scope: {
21951 col: '=',
21952 position: '@',
21953 renderIndex: '='
21954 },
21955 require: '?^uiGrid',
21956 link: function ($scope, $elm, $attrs, uiGridCtrl) {
21957 var startX = 0,
21958 x = 0,
21959 gridLeft = 0,
21960 rtlMultiplier = 1;
21961
21962 //when in RTL mode reverse the direction using the rtlMultiplier and change the position to left
21963 if (uiGridCtrl.grid.isRTL()) {
21964 $scope.position = 'left';
21965 rtlMultiplier = -1;
21966 }
21967
21968 if ($scope.position === 'left') {
21969 $elm.addClass('left');
21970 }
21971 else if ($scope.position === 'right') {
21972 $elm.addClass('right');
21973 }
21974
21975 // Refresh the grid canvas
21976 // takes an argument representing the diff along the X-axis that the resize had
21977 function refreshCanvas(xDiff) {
21978 // Then refresh the grid canvas, rebuilding the styles so that the scrollbar updates its size
21979 uiGridCtrl.grid.refreshCanvas(true).then( function() {
21980 uiGridCtrl.grid.queueGridRefresh();
21981 });
21982 }
21983
21984 // Check that the requested width isn't wider than the maxWidth, or narrower than the minWidth
21985 // Returns the new recommended with, after constraints applied
21986 function constrainWidth(col, width){
21987 var newWidth = width;
21988
21989 // If the new width would be less than the column's allowably minimum width, don't allow it
21990 if (col.minWidth && newWidth < col.minWidth) {
21991 newWidth = col.minWidth;
21992 }
21993 else if (col.maxWidth && newWidth > col.maxWidth) {
21994 newWidth = col.maxWidth;
21995 }
21996
21997 return newWidth;
21998 }
21999
22000
22001 /*
22002 * Our approach to event handling aims to deal with both touch devices and mouse devices
22003 * We register down handlers on both touch and mouse. When a touchstart or mousedown event
22004 * occurs, we register the corresponding touchmove/touchend, or mousemove/mouseend events.
22005 *
22006 * This way we can listen for both without worrying about the fact many touch devices also emulate
22007 * mouse events - basically whichever one we hear first is what we'll go with.
22008 */
22009 function moveFunction(event, args) {
22010 if (event.originalEvent) { event = event.originalEvent; }
22011 event.preventDefault();
22012
22013 x = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22014
22015 if (x < 0) { x = 0; }
22016 else if (x > uiGridCtrl.grid.gridWidth) { x = uiGridCtrl.grid.gridWidth; }
22017
22018 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22019
22020 // Don't resize if it's disabled on this column
22021 if (col.colDef.enableColumnResizing === false) {
22022 return;
22023 }
22024
22025 if (!uiGridCtrl.grid.element.hasClass('column-resizing')) {
22026 uiGridCtrl.grid.element.addClass('column-resizing');
22027 }
22028
22029 // Get the diff along the X axis
22030 var xDiff = x - startX;
22031
22032 // Get the width that this mouse would give the column
22033 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22034
22035 // check we're not outside the allowable bounds for this column
22036 x = x + ( constrainWidth(col, newWidth) - newWidth ) * rtlMultiplier;
22037
22038 resizeOverlay.css({ left: x + 'px' });
22039
22040 uiGridCtrl.fireEvent(uiGridConstants.events.ITEM_DRAGGING);
22041 }
22042
22043
22044 function upFunction(event, args) {
22045 if (event.originalEvent) { event = event.originalEvent; }
22046 event.preventDefault();
22047
22048 uiGridCtrl.grid.element.removeClass('column-resizing');
22049
22050 resizeOverlay.remove();
22051
22052 // Resize the column
22053 x = (event.changedTouches ? event.changedTouches[0] : event).clientX - gridLeft;
22054 var xDiff = x - startX;
22055
22056 if (xDiff === 0) {
22057 // no movement, so just reset event handlers, including turning back on both
22058 // down events - we turned one off when this event started
22059 offAllEvents();
22060 onDownEvents();
22061 return;
22062 }
22063
22064 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22065
22066 // Don't resize if it's disabled on this column
22067 if (col.colDef.enableColumnResizing === false) {
22068 return;
22069 }
22070
22071 // Get the new width
22072 var newWidth = parseInt(col.drawnWidth + xDiff * rtlMultiplier, 10);
22073
22074 // check we're not outside the allowable bounds for this column
22075 col.width = constrainWidth(col, newWidth);
22076 col.hasCustomWidth = true;
22077
22078 refreshCanvas(xDiff);
22079
22080 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff);
22081
22082 // stop listening of up and move events - wait for next down
22083 // reset the down events - we will have turned one off when this event started
22084 offAllEvents();
22085 onDownEvents();
22086 }
22087
22088
22089 var downFunction = function(event, args) {
22090 if (event.originalEvent) { event = event.originalEvent; }
22091 event.stopPropagation();
22092
22093 // Get the left offset of the grid
22094 // gridLeft = uiGridCtrl.grid.element[0].offsetLeft;
22095 gridLeft = uiGridCtrl.grid.element[0].getBoundingClientRect().left;
22096
22097 // Get the starting X position, which is the X coordinate of the click minus the grid's offset
22098 startX = (event.targetTouches ? event.targetTouches[0] : event).clientX - gridLeft;
22099
22100 // Append the resizer overlay
22101 uiGridCtrl.grid.element.append(resizeOverlay);
22102
22103 // Place the resizer overlay at the start position
22104 resizeOverlay.css({ left: startX });
22105
22106 // Add handlers for move and up events - if we were mousedown then we listen for mousemove and mouseup, if
22107 // we were touchdown then we listen for touchmove and touchup. Also remove the handler for the equivalent
22108 // down event - so if we're touchdown, then remove the mousedown handler until this event is over, if we're
22109 // mousedown then remove the touchdown handler until this event is over, this avoids processing duplicate events
22110 if ( event.type === 'touchstart' ){
22111 $document.on('touchend', upFunction);
22112 $document.on('touchmove', moveFunction);
22113 $elm.off('mousedown', downFunction);
22114 } else {
22115 $document.on('mouseup', upFunction);
22116 $document.on('mousemove', moveFunction);
22117 $elm.off('touchstart', downFunction);
22118 }
22119 };
22120
22121 var onDownEvents = function() {
22122 $elm.on('mousedown', downFunction);
22123 $elm.on('touchstart', downFunction);
22124 };
22125
22126 var offAllEvents = function() {
22127 $document.off('mouseup', upFunction);
22128 $document.off('touchend', upFunction);
22129 $document.off('mousemove', moveFunction);
22130 $document.off('touchmove', moveFunction);
22131 $elm.off('mousedown', downFunction);
22132 $elm.off('touchstart', downFunction);
22133 };
22134
22135 onDownEvents();
22136
22137
22138 // On doubleclick, resize to fit all rendered cells
22139 var dblClickFn = function(event, args){
22140 event.stopPropagation();
22141
22142 var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier);
22143
22144 // Don't resize if it's disabled on this column
22145 if (col.colDef.enableColumnResizing === false) {
22146 return;
22147 }
22148
22149 // Go through the rendered rows and find out the max size for the data in this column
22150 var maxWidth = 0;
22151 var xDiff = 0;
22152
22153 // Get the parent render container element
22154 var renderContainerElm = gridUtil.closestElm($elm, '.ui-grid-render-container');
22155
22156 // Get the cell contents so we measure correctly. For the header cell we have to account for the sort icon and the menu buttons, if present
22157 var cells = renderContainerElm.querySelectorAll('.' + uiGridConstants.COL_CLASS_PREFIX + col.uid + ' .ui-grid-cell-contents');
22158 Array.prototype.forEach.call(cells, function (cell) {
22159 // Get the cell width
22160 // gridUtil.logDebug('width', gridUtil.elementWidth(cell));
22161
22162 // Account for the menu button if it exists
22163 var menuButton;
22164 if (angular.element(cell).parent().hasClass('ui-grid-header-cell')) {
22165 menuButton = angular.element(cell).parent()[0].querySelectorAll('.ui-grid-column-menu-button');
22166 }
22167
22168 gridUtil.fakeElement(cell, {}, function(newElm) {
22169 // Make the element float since it's a div and can expand to fill its container
22170 var e = angular.element(newElm);
22171 e.attr('style', 'float: left');
22172
22173 var width = gridUtil.elementWidth(e);
22174
22175 if (menuButton) {
22176 var menuButtonWidth = gridUtil.elementWidth(menuButton);
22177 width = width + menuButtonWidth;
22178 }
22179
22180 if (width > maxWidth) {
22181 maxWidth = width;
22182 xDiff = maxWidth - width;
22183 }
22184 });
22185 });
22186
22187 // check we're not outside the allowable bounds for this column
22188 col.width = constrainWidth(col, maxWidth);
22189 col.hasCustomWidth = true;
22190
22191 refreshCanvas(xDiff);
22192
22193 uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); };
22194 $elm.on('dblclick', dblClickFn);
22195
22196 $elm.on('$destroy', function() {
22197 $elm.off('dblclick', dblClickFn);
22198 offAllEvents();
22199 });
22200 }
22201 };
22202
22203 return resizer;
22204 }]);
22205
22206 })();
22207
22208 (function () {
22209 'use strict';
22210
22211 /**
22212 * @ngdoc overview
22213 * @name ui.grid.rowEdit
22214 * @description
22215 *
22216 * # ui.grid.rowEdit
22217 *
22218 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
22219 *
22220 * This module extends the edit feature to provide tracking and saving of rows
22221 * of data. The tutorial provides more information on how this feature is best
22222 * used {@link tutorial/205_row_editable here}.
22223 * <br/>
22224 * This feature depends on usage of the ui-grid-edit feature, and also benefits
22225 * from use of ui-grid-cellNav to provide the full spreadsheet-like editing
22226 * experience
22227 *
22228 */
22229
22230 var module = angular.module('ui.grid.rowEdit', ['ui.grid', 'ui.grid.edit', 'ui.grid.cellNav']);
22231
22232 /**
22233 * @ngdoc object
22234 * @name ui.grid.rowEdit.constant:uiGridRowEditConstants
22235 *
22236 * @description constants available in row edit module
22237 */
22238 module.constant('uiGridRowEditConstants', {
22239 });
22240
22241 /**
22242 * @ngdoc service
22243 * @name ui.grid.rowEdit.service:uiGridRowEditService
22244 *
22245 * @description Services for row editing features
22246 */
22247 module.service('uiGridRowEditService', ['$interval', '$q', 'uiGridConstants', 'uiGridRowEditConstants', 'gridUtil',
22248 function ($interval, $q, uiGridConstants, uiGridRowEditConstants, gridUtil) {
22249
22250 var service = {
22251
22252 initializeGrid: function (scope, grid) {
22253 /**
22254 * @ngdoc object
22255 * @name ui.grid.rowEdit.api:PublicApi
22256 *
22257 * @description Public Api for rowEdit feature
22258 */
22259
22260 grid.rowEdit = {};
22261
22262 var publicApi = {
22263 events: {
22264 rowEdit: {
22265 /**
22266 * @ngdoc event
22267 * @eventOf ui.grid.rowEdit.api:PublicApi
22268 * @name saveRow
22269 * @description raised when a row is ready for saving. Once your
22270 * row has saved you may need to use angular.extend to update the
22271 * data entity with any changed data from your save (for example,
22272 * lock version information if you're using optimistic locking,
22273 * or last update time/user information).
22274 *
22275 * Your method should call setSavePromise somewhere in the body before
22276 * returning control. The feature will then wait, with the gridRow greyed out
22277 * whilst this promise is being resolved.
22278 *
22279 * <pre>
22280 * gridApi.rowEdit.on.saveRow(scope,function(rowEntity){})
22281 * </pre>
22282 * and somewhere within the event handler:
22283 * <pre>
22284 * gridApi.rowEdit.setSavePromise( rowEntity, savePromise)
22285 * </pre>
22286 * @param {object} rowEntity the options.data element that was edited
22287 * @returns {promise} Your saveRow method should return a promise, the
22288 * promise should either be resolved (implying successful save), or
22289 * rejected (implying an error).
22290 */
22291 saveRow: function (rowEntity) {
22292 }
22293 }
22294 },
22295 methods: {
22296 rowEdit: {
22297 /**
22298 * @ngdoc method
22299 * @methodOf ui.grid.rowEdit.api:PublicApi
22300 * @name setSavePromise
22301 * @description Sets the promise associated with the row save, mandatory that
22302 * the saveRow event handler calls this method somewhere before returning.
22303 * <pre>
22304 * gridApi.rowEdit.setSavePromise(rowEntity, savePromise)
22305 * </pre>
22306 * @param {object} rowEntity a data row from the grid for which a save has
22307 * been initiated
22308 * @param {promise} savePromise the promise that will be resolved when the
22309 * save is successful, or rejected if the save fails
22310 *
22311 */
22312 setSavePromise: function ( rowEntity, savePromise) {
22313 service.setSavePromise(grid, rowEntity, savePromise);
22314 },
22315 /**
22316 * @ngdoc method
22317 * @methodOf ui.grid.rowEdit.api:PublicApi
22318 * @name getDirtyRows
22319 * @description Returns all currently dirty rows
22320 * <pre>
22321 * gridApi.rowEdit.getDirtyRows(grid)
22322 * </pre>
22323 * @returns {array} An array of gridRows that are currently dirty
22324 *
22325 */
22326 getDirtyRows: function () {
22327 return grid.rowEdit.dirtyRows ? grid.rowEdit.dirtyRows : [];
22328 },
22329 /**
22330 * @ngdoc method
22331 * @methodOf ui.grid.rowEdit.api:PublicApi
22332 * @name getErrorRows
22333 * @description Returns all currently errored rows
22334 * <pre>
22335 * gridApi.rowEdit.getErrorRows(grid)
22336 * </pre>
22337 * @returns {array} An array of gridRows that are currently in error
22338 *
22339 */
22340 getErrorRows: function () {
22341 return grid.rowEdit.errorRows ? grid.rowEdit.errorRows : [];
22342 },
22343 /**
22344 * @ngdoc method
22345 * @methodOf ui.grid.rowEdit.api:PublicApi
22346 * @name flushDirtyRows
22347 * @description Triggers a save event for all currently dirty rows, could
22348 * be used where user presses a save button or navigates away from the page
22349 * <pre>
22350 * gridApi.rowEdit.flushDirtyRows(grid)
22351 * </pre>
22352 * @returns {promise} a promise that represents the aggregate of all
22353 * of the individual save promises - i.e. it will be resolved when all
22354 * the individual save promises have been resolved.
22355 *
22356 */
22357 flushDirtyRows: function () {
22358 return service.flushDirtyRows(grid);
22359 },
22360
22361 /**
22362 * @ngdoc method
22363 * @methodOf ui.grid.rowEdit.api:PublicApi
22364 * @name setRowsDirty
22365 * @description Sets each of the rows passed in dataRows
22366 * to be dirty. note that if you have only just inserted the
22367 * rows into your data you will need to wait for a $digest cycle
22368 * before the gridRows are present - so often you would wrap this
22369 * call in a $interval or $timeout
22370 * <pre>
22371 * $interval( function() {
22372 * gridApi.rowEdit.setRowsDirty(myDataRows);
22373 * }, 0, 1);
22374 * </pre>
22375 * @param {array} dataRows the data entities for which the gridRows
22376 * should be set dirty.
22377 *
22378 */
22379 setRowsDirty: function ( dataRows) {
22380 service.setRowsDirty(grid, dataRows);
22381 },
22382
22383 /**
22384 * @ngdoc method
22385 * @methodOf ui.grid.rowEdit.api:PublicApi
22386 * @name setRowsClean
22387 * @description Sets each of the rows passed in dataRows
22388 * to be clean, removing them from the dirty cache and the error cache,
22389 * and clearing the error flag and the dirty flag
22390 * <pre>
22391 * var gridRows = $scope.gridApi.rowEdit.getDirtyRows();
22392 * var dataRows = gridRows.map( function( gridRow ) { return gridRow.entity; });
22393 * $scope.gridApi.rowEdit.setRowsClean( dataRows );
22394 * </pre>
22395 * @param {array} dataRows the data entities for which the gridRows
22396 * should be set clean.
22397 *
22398 */
22399 setRowsClean: function ( dataRows) {
22400 service.setRowsClean(grid, dataRows);
22401 }
22402 }
22403 }
22404 };
22405
22406 grid.api.registerEventsFromObject(publicApi.events);
22407 grid.api.registerMethodsFromObject(publicApi.methods);
22408
22409 grid.api.core.on.renderingComplete( scope, function ( gridApi ) {
22410 grid.api.edit.on.afterCellEdit( scope, service.endEditCell );
22411 grid.api.edit.on.beginCellEdit( scope, service.beginEditCell );
22412 grid.api.edit.on.cancelCellEdit( scope, service.cancelEditCell );
22413
22414 if ( grid.api.cellNav ) {
22415 grid.api.cellNav.on.navigate( scope, service.navigate );
22416 }
22417 });
22418
22419 },
22420
22421 defaultGridOptions: function (gridOptions) {
22422
22423 /**
22424 * @ngdoc object
22425 * @name ui.grid.rowEdit.api:GridOptions
22426 *
22427 * @description Options for configuring the rowEdit feature, these are available to be
22428 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
22429 */
22430
22431 },
22432
22433
22434 /**
22435 * @ngdoc method
22436 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22437 * @name saveRow
22438 * @description Returns a function that saves the specified row from the grid,
22439 * and returns a promise
22440 * @param {object} grid the grid for which dirty rows should be flushed
22441 * @param {GridRow} gridRow the row that should be saved
22442 * @returns {function} the saveRow function returns a function. That function
22443 * in turn, when called, returns a promise relating to the save callback
22444 */
22445 saveRow: function ( grid, gridRow ) {
22446 var self = this;
22447
22448 return function() {
22449 gridRow.isSaving = true;
22450
22451 if ( gridRow.rowEditSavePromise ){
22452 // don't save the row again if it's already saving - that causes stale object exceptions
22453 return gridRow.rowEditSavePromise;
22454 }
22455
22456 var promise = grid.api.rowEdit.raise.saveRow( gridRow.entity );
22457
22458 if ( gridRow.rowEditSavePromise ){
22459 gridRow.rowEditSavePromise.then( self.processSuccessPromise( grid, gridRow ), self.processErrorPromise( grid, gridRow ));
22460 } else {
22461 gridUtil.logError( 'A promise was not returned when saveRow event was raised, either nobody is listening to event, or event handler did not return a promise' );
22462 }
22463 return promise;
22464 };
22465 },
22466
22467
22468 /**
22469 * @ngdoc method
22470 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22471 * @name setSavePromise
22472 * @description Sets the promise associated with the row save, mandatory that
22473 * the saveRow event handler calls this method somewhere before returning.
22474 * <pre>
22475 * gridApi.rowEdit.setSavePromise(grid, rowEntity)
22476 * </pre>
22477 * @param {object} grid the grid for which dirty rows should be returned
22478 * @param {object} rowEntity a data row from the grid for which a save has
22479 * been initiated
22480 * @param {promise} savePromise the promise that will be resolved when the
22481 * save is successful, or rejected if the save fails
22482 *
22483 */
22484 setSavePromise: function (grid, rowEntity, savePromise) {
22485 var gridRow = grid.getRow( rowEntity );
22486 gridRow.rowEditSavePromise = savePromise;
22487 },
22488
22489
22490 /**
22491 * @ngdoc method
22492 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22493 * @name processSuccessPromise
22494 * @description Returns a function that processes the successful
22495 * resolution of a save promise
22496 * @param {object} grid the grid for which the promise should be processed
22497 * @param {GridRow} gridRow the row that has been saved
22498 * @returns {function} the success handling function
22499 */
22500 processSuccessPromise: function ( grid, gridRow ) {
22501 var self = this;
22502
22503 return function() {
22504 delete gridRow.isSaving;
22505 delete gridRow.isDirty;
22506 delete gridRow.isError;
22507 delete gridRow.rowEditSaveTimer;
22508 delete gridRow.rowEditSavePromise;
22509 self.removeRow( grid.rowEdit.errorRows, gridRow );
22510 self.removeRow( grid.rowEdit.dirtyRows, gridRow );
22511 };
22512 },
22513
22514
22515 /**
22516 * @ngdoc method
22517 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22518 * @name processErrorPromise
22519 * @description Returns a function that processes the failed
22520 * resolution of a save promise
22521 * @param {object} grid the grid for which the promise should be processed
22522 * @param {GridRow} gridRow the row that is now in error
22523 * @returns {function} the error handling function
22524 */
22525 processErrorPromise: function ( grid, gridRow ) {
22526 return function() {
22527 delete gridRow.isSaving;
22528 delete gridRow.rowEditSaveTimer;
22529 delete gridRow.rowEditSavePromise;
22530
22531 gridRow.isError = true;
22532
22533 if (!grid.rowEdit.errorRows){
22534 grid.rowEdit.errorRows = [];
22535 }
22536 if (!service.isRowPresent( grid.rowEdit.errorRows, gridRow ) ){
22537 grid.rowEdit.errorRows.push( gridRow );
22538 }
22539 };
22540 },
22541
22542
22543 /**
22544 * @ngdoc method
22545 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22546 * @name removeRow
22547 * @description Removes a row from a cache of rows - either
22548 * grid.rowEdit.errorRows or grid.rowEdit.dirtyRows. If the row
22549 * is not present silently does nothing.
22550 * @param {array} rowArray the array from which to remove the row
22551 * @param {GridRow} gridRow the row that should be removed
22552 */
22553 removeRow: function( rowArray, removeGridRow ){
22554 if (typeof(rowArray) === 'undefined' || rowArray === null){
22555 return;
22556 }
22557
22558 rowArray.forEach( function( gridRow, index ){
22559 if ( gridRow.uid === removeGridRow.uid ){
22560 rowArray.splice( index, 1);
22561 }
22562 });
22563 },
22564
22565
22566 /**
22567 * @ngdoc method
22568 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22569 * @name isRowPresent
22570 * @description Checks whether a row is already present
22571 * in the given array
22572 * @param {array} rowArray the array in which to look for the row
22573 * @param {GridRow} gridRow the row that should be looked for
22574 */
22575 isRowPresent: function( rowArray, removeGridRow ){
22576 var present = false;
22577 rowArray.forEach( function( gridRow, index ){
22578 if ( gridRow.uid === removeGridRow.uid ){
22579 present = true;
22580 }
22581 });
22582 return present;
22583 },
22584
22585
22586 /**
22587 * @ngdoc method
22588 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22589 * @name flushDirtyRows
22590 * @description Triggers a save event for all currently dirty rows, could
22591 * be used where user presses a save button or navigates away from the page
22592 * <pre>
22593 * gridApi.rowEdit.flushDirtyRows(grid)
22594 * </pre>
22595 * @param {object} grid the grid for which dirty rows should be flushed
22596 * @returns {promise} a promise that represents the aggregate of all
22597 * of the individual save promises - i.e. it will be resolved when all
22598 * the individual save promises have been resolved.
22599 *
22600 */
22601 flushDirtyRows: function(grid){
22602 var promises = [];
22603 grid.api.rowEdit.getDirtyRows().forEach( function( gridRow ){
22604 service.saveRow( grid, gridRow )();
22605 promises.push( gridRow.rowEditSavePromise );
22606 });
22607
22608 return $q.all( promises );
22609 },
22610
22611
22612 /**
22613 * @ngdoc method
22614 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22615 * @name endEditCell
22616 * @description Receives an afterCellEdit event from the edit function,
22617 * and sets flags as appropriate. Only the rowEntity parameter
22618 * is processed, although other params are available. Grid
22619 * is automatically provided by the gridApi.
22620 * @param {object} rowEntity the data entity for which the cell
22621 * was edited
22622 */
22623 endEditCell: function( rowEntity, colDef, newValue, previousValue ){
22624 var grid = this.grid;
22625 var gridRow = grid.getRow( rowEntity );
22626 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, dirty flag cannot be set' ); return; }
22627
22628 if ( newValue !== previousValue || gridRow.isDirty ){
22629 if ( !grid.rowEdit.dirtyRows ){
22630 grid.rowEdit.dirtyRows = [];
22631 }
22632
22633 if ( !gridRow.isDirty ){
22634 gridRow.isDirty = true;
22635 grid.rowEdit.dirtyRows.push( gridRow );
22636 }
22637
22638 delete gridRow.isError;
22639
22640 service.considerSetTimer( grid, gridRow );
22641 }
22642 },
22643
22644
22645 /**
22646 * @ngdoc method
22647 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22648 * @name beginEditCell
22649 * @description Receives a beginCellEdit event from the edit function,
22650 * and cancels any rowEditSaveTimers if present, as the user is still editing
22651 * this row. Only the rowEntity parameter
22652 * is processed, although other params are available. Grid
22653 * is automatically provided by the gridApi.
22654 * @param {object} rowEntity the data entity for which the cell
22655 * editing has commenced
22656 */
22657 beginEditCell: function( rowEntity, colDef ){
22658 var grid = this.grid;
22659 var gridRow = grid.getRow( rowEntity );
22660 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be cancelled' ); return; }
22661
22662 service.cancelTimer( grid, gridRow );
22663 },
22664
22665
22666 /**
22667 * @ngdoc method
22668 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22669 * @name cancelEditCell
22670 * @description Receives a cancelCellEdit event from the edit function,
22671 * and if the row was already dirty, restarts the save timer. If the row
22672 * was not already dirty, then it's not dirty now either and does nothing.
22673 *
22674 * Only the rowEntity parameter
22675 * is processed, although other params are available. Grid
22676 * is automatically provided by the gridApi.
22677 *
22678 * @param {object} rowEntity the data entity for which the cell
22679 * editing was cancelled
22680 */
22681 cancelEditCell: function( rowEntity, colDef ){
22682 var grid = this.grid;
22683 var gridRow = grid.getRow( rowEntity );
22684 if ( !gridRow ){ gridUtil.logError( 'Unable to find rowEntity in grid data, timer cannot be set' ); return; }
22685
22686 service.considerSetTimer( grid, gridRow );
22687 },
22688
22689
22690 /**
22691 * @ngdoc method
22692 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22693 * @name navigate
22694 * @description cellNav tells us that the selected cell has changed. If
22695 * the new row had a timer running, then stop it similar to in a beginCellEdit
22696 * call. If the old row is dirty and not the same as the new row, then
22697 * start a timer on it.
22698 * @param {object} newRowCol the row and column that were selected
22699 * @param {object} oldRowCol the row and column that was left
22700 *
22701 */
22702 navigate: function( newRowCol, oldRowCol ){
22703 var grid = this.grid;
22704 if ( newRowCol.row.rowEditSaveTimer ){
22705 service.cancelTimer( grid, newRowCol.row );
22706 }
22707
22708 if ( oldRowCol && oldRowCol.row && oldRowCol.row !== newRowCol.row ){
22709 service.considerSetTimer( grid, oldRowCol.row );
22710 }
22711 },
22712
22713
22714 /**
22715 * @ngdoc property
22716 * @propertyOf ui.grid.rowEdit.api:GridOptions
22717 * @name rowEditWaitInterval
22718 * @description How long the grid should wait for another change on this row
22719 * before triggering a save (in milliseconds). If set to -1, then saves are
22720 * never triggered by timer (implying that the user will call flushDirtyRows()
22721 * manually)
22722 *
22723 * @example
22724 * Setting the wait interval to 4 seconds
22725 * <pre>
22726 * $scope.gridOptions = { rowEditWaitInterval: 4000 }
22727 * </pre>
22728 *
22729 */
22730 /**
22731 * @ngdoc method
22732 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22733 * @name considerSetTimer
22734 * @description Consider setting a timer on this row (if it is dirty). if there is a timer running
22735 * on the row and the row isn't currently saving, cancel it, using cancelTimer, then if the row is
22736 * dirty and not currently saving then set a new timer
22737 * @param {object} grid the grid for which we are processing
22738 * @param {GridRow} gridRow the row for which the timer should be adjusted
22739 *
22740 */
22741 considerSetTimer: function( grid, gridRow ){
22742 service.cancelTimer( grid, gridRow );
22743
22744 if ( gridRow.isDirty && !gridRow.isSaving ){
22745 if ( grid.options.rowEditWaitInterval !== -1 ){
22746 var waitTime = grid.options.rowEditWaitInterval ? grid.options.rowEditWaitInterval : 2000;
22747 gridRow.rowEditSaveTimer = $interval( service.saveRow( grid, gridRow ), waitTime, 1);
22748 }
22749 }
22750 },
22751
22752
22753 /**
22754 * @ngdoc method
22755 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22756 * @name cancelTimer
22757 * @description cancel the $interval for any timer running on this row
22758 * then delete the timer itself
22759 * @param {object} grid the grid for which we are processing
22760 * @param {GridRow} gridRow the row for which the timer should be adjusted
22761 *
22762 */
22763 cancelTimer: function( grid, gridRow ){
22764 if ( gridRow.rowEditSaveTimer && !gridRow.isSaving ){
22765 $interval.cancel(gridRow.rowEditSaveTimer);
22766 delete gridRow.rowEditSaveTimer;
22767 }
22768 },
22769
22770
22771 /**
22772 * @ngdoc method
22773 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22774 * @name setRowsDirty
22775 * @description Sets each of the rows passed in dataRows
22776 * to be dirty. note that if you have only just inserted the
22777 * rows into your data you will need to wait for a $digest cycle
22778 * before the gridRows are present - so often you would wrap this
22779 * call in a $interval or $timeout
22780 * <pre>
22781 * $interval( function() {
22782 * gridApi.rowEdit.setRowsDirty( myDataRows);
22783 * }, 0, 1);
22784 * </pre>
22785 * @param {object} grid the grid for which rows should be set dirty
22786 * @param {array} dataRows the data entities for which the gridRows
22787 * should be set dirty.
22788 *
22789 */
22790 setRowsDirty: function( grid, myDataRows ) {
22791 var gridRow;
22792 myDataRows.forEach( function( value, index ){
22793 gridRow = grid.getRow( value );
22794 if ( gridRow ){
22795 if ( !grid.rowEdit.dirtyRows ){
22796 grid.rowEdit.dirtyRows = [];
22797 }
22798
22799 if ( !gridRow.isDirty ){
22800 gridRow.isDirty = true;
22801 grid.rowEdit.dirtyRows.push( gridRow );
22802 }
22803
22804 delete gridRow.isError;
22805
22806 service.considerSetTimer( grid, gridRow );
22807 } else {
22808 gridUtil.logError( "requested row not found in rowEdit.setRowsDirty, row was: " + value );
22809 }
22810 });
22811 },
22812
22813
22814 /**
22815 * @ngdoc method
22816 * @methodOf ui.grid.rowEdit.service:uiGridRowEditService
22817 * @name setRowsClean
22818 * @description Sets each of the rows passed in dataRows
22819 * to be clean, clearing the dirty flag and the error flag, and removing
22820 * the rows from the dirty and error caches.
22821 * @param {object} grid the grid for which rows should be set clean
22822 * @param {array} dataRows the data entities for which the gridRows
22823 * should be set clean.
22824 *
22825 */
22826 setRowsClean: function( grid, myDataRows ) {
22827 var gridRow;
22828
22829 myDataRows.forEach( function( value, index ){
22830 gridRow = grid.getRow( value );
22831 if ( gridRow ){
22832 delete gridRow.isDirty;
22833 service.removeRow( grid.rowEdit.dirtyRows, gridRow );
22834 service.cancelTimer( grid, gridRow );
22835
22836 delete gridRow.isError;
22837 service.removeRow( grid.rowEdit.errorRows, gridRow );
22838 } else {
22839 gridUtil.logError( "requested row not found in rowEdit.setRowsClean, row was: " + value );
22840 }
22841 });
22842 }
22843
22844 };
22845
22846 return service;
22847
22848 }]);
22849
22850 /**
22851 * @ngdoc directive
22852 * @name ui.grid.rowEdit.directive:uiGridEdit
22853 * @element div
22854 * @restrict A
22855 *
22856 * @description Adds row editing features to the ui-grid-edit directive.
22857 *
22858 */
22859 module.directive('uiGridRowEdit', ['gridUtil', 'uiGridRowEditService', 'uiGridEditConstants',
22860 function (gridUtil, uiGridRowEditService, uiGridEditConstants) {
22861 return {
22862 replace: true,
22863 priority: 0,
22864 require: '^uiGrid',
22865 scope: false,
22866 compile: function () {
22867 return {
22868 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
22869 uiGridRowEditService.initializeGrid($scope, uiGridCtrl.grid);
22870 },
22871 post: function ($scope, $elm, $attrs, uiGridCtrl) {
22872 }
22873 };
22874 }
22875 };
22876 }]);
22877
22878
22879 /**
22880 * @ngdoc directive
22881 * @name ui.grid.rowEdit.directive:uiGridViewport
22882 * @element div
22883 *
22884 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
22885 * for the grid row to allow coloring of saving and error rows
22886 */
22887 module.directive('uiGridViewport',
22888 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
22889 function ($compile, uiGridConstants, gridUtil, $parse) {
22890 return {
22891 priority: -200, // run after default directive
22892 scope: false,
22893 compile: function ($elm, $attrs) {
22894 var rowRepeatDiv = angular.element($elm.children().children()[0]);
22895
22896 var existingNgClass = rowRepeatDiv.attr("ng-class");
22897 var newNgClass = '';
22898 if ( existingNgClass ) {
22899 newNgClass = existingNgClass.slice(0, -1) + ", 'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
22900 } else {
22901 newNgClass = "{'ui-grid-row-dirty': row.isDirty, 'ui-grid-row-saving': row.isSaving, 'ui-grid-row-error': row.isError}";
22902 }
22903 rowRepeatDiv.attr("ng-class", newNgClass);
22904
22905 return {
22906 pre: function ($scope, $elm, $attrs, controllers) {
22907
22908 },
22909 post: function ($scope, $elm, $attrs, controllers) {
22910 }
22911 };
22912 }
22913 };
22914 }]);
22915
22916 })();
22917
22918 (function () {
22919 'use strict';
22920
22921 /**
22922 * @ngdoc overview
22923 * @name ui.grid.saveState
22924 * @description
22925 *
22926 * # ui.grid.saveState
22927 *
22928 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
22929 *
22930 * This module provides the ability to save the grid state, and restore
22931 * it when the user returns to the page.
22932 *
22933 * No UI is provided, the caller should provide their own UI/buttons
22934 * as appropriate. Usually the navigate events would be used to save
22935 * the grid state and restore it.
22936 *
22937 * <br/>
22938 * <br/>
22939 *
22940 * <div doc-module-components="ui.grid.save-state"></div>
22941 */
22942
22943 var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning', 'ui.grid.treeView']);
22944
22945 /**
22946 * @ngdoc object
22947 * @name ui.grid.saveState.constant:uiGridSaveStateConstants
22948 *
22949 * @description constants available in save state module
22950 */
22951
22952 module.constant('uiGridSaveStateConstants', {
22953 featureName: 'saveState'
22954 });
22955
22956 /**
22957 * @ngdoc service
22958 * @name ui.grid.saveState.service:uiGridSaveStateService
22959 *
22960 * @description Services for saveState feature
22961 */
22962 module.service('uiGridSaveStateService', ['$q', 'uiGridSaveStateConstants', 'gridUtil', '$compile', '$interval', 'uiGridConstants',
22963 function ($q, uiGridSaveStateConstants, gridUtil, $compile, $interval, uiGridConstants ) {
22964
22965 var service = {
22966
22967 initializeGrid: function (grid) {
22968
22969 //add feature namespace and any properties to grid for needed state
22970 grid.saveState = {};
22971 this.defaultGridOptions(grid.options);
22972
22973 /**
22974 * @ngdoc object
22975 * @name ui.grid.saveState.api:PublicApi
22976 *
22977 * @description Public Api for saveState feature
22978 */
22979 var publicApi = {
22980 events: {
22981 saveState: {
22982 }
22983 },
22984 methods: {
22985 saveState: {
22986 /**
22987 * @ngdoc function
22988 * @name save
22989 * @methodOf ui.grid.saveState.api:PublicApi
22990 * @description Packages the current state of the grid into
22991 * an object, and provides it to the user for saving
22992 * @returns {object} the state as a javascript object that can be saved
22993 */
22994 save: function () {
22995 return service.save(grid);
22996 },
22997 /**
22998 * @ngdoc function
22999 * @name restore
23000 * @methodOf ui.grid.saveState.api:PublicApi
23001 * @description Restores the provided state into the grid
23002 * @param {scope} $scope a scope that we can broadcast on
23003 * @param {object} state the state that should be restored into the grid
23004 */
23005 restore: function ( $scope, state) {
23006 service.restore(grid, $scope, state);
23007 }
23008 }
23009 }
23010 };
23011
23012 grid.api.registerEventsFromObject(publicApi.events);
23013
23014 grid.api.registerMethodsFromObject(publicApi.methods);
23015
23016 },
23017
23018 defaultGridOptions: function (gridOptions) {
23019 //default option to true unless it was explicitly set to false
23020 /**
23021 * @ngdoc object
23022 * @name ui.grid.saveState.api:GridOptions
23023 *
23024 * @description GridOptions for saveState feature, these are available to be
23025 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
23026 */
23027 /**
23028 * @ngdoc object
23029 * @name saveWidths
23030 * @propertyOf ui.grid.saveState.api:GridOptions
23031 * @description Save the current column widths. Note that unless
23032 * you've provided the user with some way to resize their columns (say
23033 * the resize columns feature), then this makes little sense.
23034 * <br/>Defaults to true
23035 */
23036 gridOptions.saveWidths = gridOptions.saveWidths !== false;
23037 /**
23038 * @ngdoc object
23039 * @name saveOrder
23040 * @propertyOf ui.grid.saveState.api:GridOptions
23041 * @description Restore the current column order. Note that unless
23042 * you've provided the user with some way to reorder their columns (for
23043 * example the move columns feature), this makes little sense.
23044 * <br/>Defaults to true
23045 */
23046 gridOptions.saveOrder = gridOptions.saveOrder !== false;
23047 /**
23048 * @ngdoc object
23049 * @name saveScroll
23050 * @propertyOf ui.grid.saveState.api:GridOptions
23051 * @description Save the current scroll position. Note that this
23052 * is saved as the percentage of the grid scrolled - so if your
23053 * user returns to a grid with a significantly different number of
23054 * rows (perhaps some data has been deleted) then the scroll won't
23055 * actually show the same rows as before. If you want to scroll to
23056 * a specific row then you should instead use the saveFocus option, which
23057 * is the default.
23058 *
23059 * Note that this element will only be saved if the cellNav feature is
23060 * enabled
23061 * <br/>Defaults to false
23062 */
23063 gridOptions.saveScroll = gridOptions.saveScroll === true;
23064 /**
23065 * @ngdoc object
23066 * @name saveFocus
23067 * @propertyOf ui.grid.saveState.api:GridOptions
23068 * @description Save the current focused cell. On returning
23069 * to this focused cell we'll also scroll. This option is
23070 * preferred to the saveScroll option, so is set to true by
23071 * default. If saveScroll is set to true then this option will
23072 * be disabled.
23073 *
23074 * By default this option saves the current row number and column
23075 * number, and returns to that row and column. However, if you define
23076 * a saveRowIdentity function, then it will return you to the currently
23077 * selected column within that row (in a business sense - so if some
23078 * rows have been deleted, it will still find the same data, presuming it
23079 * still exists in the list. If it isn't in the list then it will instead
23080 * return to the same row number - i.e. scroll percentage)
23081 *
23082 * Note that this option will do nothing if the cellNav
23083 * feature is not enabled.
23084 *
23085 * <br/>Defaults to true (unless saveScroll is true)
23086 */
23087 gridOptions.saveFocus = gridOptions.saveScroll !== true && gridOptions.saveFocus !== false;
23088 /**
23089 * @ngdoc object
23090 * @name saveRowIdentity
23091 * @propertyOf ui.grid.saveState.api:GridOptions
23092 * @description A function that can be called, passing in a rowEntity,
23093 * and that will return a unique id for that row. This might simply
23094 * return the `id` field from that row (if you have one), or it might
23095 * concatenate some fields within the row to make a unique value.
23096 *
23097 * This value will be used to find the same row again and set the focus
23098 * to it, if it exists when we return.
23099 *
23100 * <br/>Defaults to undefined
23101 */
23102 /**
23103 * @ngdoc object
23104 * @name saveVisible
23105 * @propertyOf ui.grid.saveState.api:GridOptions
23106 * @description Save whether or not columns are visible.
23107 *
23108 * <br/>Defaults to true
23109 */
23110 gridOptions.saveVisible = gridOptions.saveVisible !== false;
23111 /**
23112 * @ngdoc object
23113 * @name saveSort
23114 * @propertyOf ui.grid.saveState.api:GridOptions
23115 * @description Save the current sort state for each column
23116 *
23117 * <br/>Defaults to true
23118 */
23119 gridOptions.saveSort = gridOptions.saveSort !== false;
23120 /**
23121 * @ngdoc object
23122 * @name saveFilter
23123 * @propertyOf ui.grid.saveState.api:GridOptions
23124 * @description Save the current filter state for each column
23125 *
23126 * <br/>Defaults to true
23127 */
23128 gridOptions.saveFilter = gridOptions.saveFilter !== false;
23129 /**
23130 * @ngdoc object
23131 * @name saveSelection
23132 * @propertyOf ui.grid.saveState.api:GridOptions
23133 * @description Save the currently selected rows. If the `saveRowIdentity` callback
23134 * is defined, then it will save the id of the row and select that. If not, then
23135 * it will attempt to select the rows by row number, which will give the wrong results
23136 * if the data set has changed in the mean-time.
23137 *
23138 * Note that this option only does anything
23139 * if the selection feature is enabled.
23140 *
23141 * <br/>Defaults to true
23142 */
23143 gridOptions.saveSelection = gridOptions.saveSelection !== false;
23144 /**
23145 * @ngdoc object
23146 * @name saveGrouping
23147 * @propertyOf ui.grid.saveState.api:GridOptions
23148 * @description Save the grouping configuration. If set to true and the
23149 * grouping feature is not enabled then does nothing.
23150 *
23151 * <br/>Defaults to true
23152 */
23153 gridOptions.saveGrouping = gridOptions.saveGrouping !== false;
23154 /**
23155 * @ngdoc object
23156 * @name saveGroupingExpandedStates
23157 * @propertyOf ui.grid.saveState.api:GridOptions
23158 * @description Save the grouping row expanded states. If set to true and the
23159 * grouping feature is not enabled then does nothing.
23160 *
23161 * This can be quite a bit of data, in many cases you wouldn't want to save this
23162 * information.
23163 *
23164 * <br/>Defaults to false
23165 */
23166 gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true;
23167 /**
23168 * @ngdoc object
23169 * @name savePinning
23170 * @propertyOf ui.grid.saveState.api:GridOptions
23171 * @description Save pinning state for columns.
23172 *
23173 * <br/>Defaults to true
23174 */
23175 gridOptions.savePinning = gridOptions.savePinning !== false;
23176 /**
23177 * @ngdoc object
23178 * @name saveTreeView
23179 * @propertyOf ui.grid.saveState.api:GridOptions
23180 * @description Save the treeView configuration. If set to true and the
23181 * treeView feature is not enabled then does nothing.
23182 *
23183 * <br/>Defaults to true
23184 */
23185 gridOptions.saveTreeView = gridOptions.saveTreeView !== false;
23186 },
23187
23188
23189
23190 /**
23191 * @ngdoc function
23192 * @name save
23193 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23194 * @description Saves the current grid state into an object, and
23195 * passes that object back to the caller
23196 * @param {Grid} grid the grid whose state we'd like to save
23197 * @returns {object} the state ready to be saved
23198 */
23199 save: function (grid) {
23200 var savedState = {};
23201
23202 savedState.columns = service.saveColumns( grid );
23203 savedState.scrollFocus = service.saveScrollFocus( grid );
23204 savedState.selection = service.saveSelection( grid );
23205 savedState.grouping = service.saveGrouping( grid );
23206 savedState.treeView = service.saveTreeView( grid );
23207
23208 return savedState;
23209 },
23210
23211
23212 /**
23213 * @ngdoc function
23214 * @name restore
23215 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23216 * @description Applies the provided state to the grid
23217 *
23218 * @param {Grid} grid the grid whose state we'd like to restore
23219 * @param {scope} $scope a scope that we can broadcast on
23220 * @param {object} state the state we'd like to restore
23221 */
23222 restore: function( grid, $scope, state ){
23223 if ( state.columns ) {
23224 service.restoreColumns( grid, state.columns );
23225 }
23226
23227 if ( state.scrollFocus ){
23228 service.restoreScrollFocus( grid, $scope, state.scrollFocus );
23229 }
23230
23231 if ( state.selection ){
23232 service.restoreSelection( grid, state.selection );
23233 }
23234
23235 if ( state.grouping ){
23236 service.restoreGrouping( grid, state.grouping );
23237 }
23238
23239 if ( state.treeView ){
23240 service.restoreTreeView( grid, state.treeView );
23241 }
23242
23243 grid.refresh();
23244 },
23245
23246
23247 /**
23248 * @ngdoc function
23249 * @name saveColumns
23250 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23251 * @description Saves the column setup, including sort, filters, ordering,
23252 * pinning and column widths.
23253 *
23254 * Works through the current columns, storing them in order. Stores the
23255 * column name, then the visible flag, width, sort and filters for each column.
23256 *
23257 * @param {Grid} grid the grid whose state we'd like to save
23258 * @returns {array} the columns state ready to be saved
23259 */
23260 saveColumns: function( grid ) {
23261 var columns = [];
23262 grid.getOnlyDataColumns().forEach( function( column ) {
23263 var savedColumn = {};
23264 savedColumn.name = column.name;
23265
23266 if ( grid.options.saveVisible ){
23267 savedColumn.visible = column.visible;
23268 }
23269
23270 if ( grid.options.saveWidths ){
23271 savedColumn.width = column.width;
23272 }
23273
23274 // these two must be copied, not just pointed too - otherwise our saved state is pointing to the same object as current state
23275 if ( grid.options.saveSort ){
23276 savedColumn.sort = angular.copy( column.sort );
23277 }
23278
23279 if ( grid.options.saveFilter ){
23280 savedColumn.filters = [];
23281 column.filters.forEach( function( filter ){
23282 var copiedFilter = {};
23283 angular.forEach( filter, function( value, key) {
23284 if ( key !== 'condition' && key !== '$$hashKey' && key !== 'placeholder'){
23285 copiedFilter[key] = value;
23286 }
23287 });
23288 savedColumn.filters.push(copiedFilter);
23289 });
23290 }
23291
23292 if ( !!grid.api.pinning && grid.options.savePinning ){
23293 savedColumn.pinned = column.renderContainer ? column.renderContainer : '';
23294 }
23295
23296 columns.push( savedColumn );
23297 });
23298
23299 return columns;
23300 },
23301
23302
23303 /**
23304 * @ngdoc function
23305 * @name saveScrollFocus
23306 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23307 * @description Saves the currently scroll or focus.
23308 *
23309 * If cellNav isn't present then does nothing - we can't return
23310 * to the scroll position without cellNav anyway.
23311 *
23312 * If the cellNav module is present, and saveFocus is true, then
23313 * it saves the currently focused cell. If rowIdentity is present
23314 * then saves using rowIdentity, otherwise saves visibleRowNum.
23315 *
23316 * If the cellNav module is not present, and saveScroll is true, then
23317 * it approximates the current scroll row and column, and saves that.
23318 *
23319 * @param {Grid} grid the grid whose state we'd like to save
23320 * @returns {object} the selection state ready to be saved
23321 */
23322 saveScrollFocus: function( grid ){
23323 if ( !grid.api.cellNav ){
23324 return {};
23325 }
23326
23327 var scrollFocus = {};
23328 if ( grid.options.saveFocus ){
23329 scrollFocus.focus = true;
23330 var rowCol = grid.api.cellNav.getFocusedCell();
23331 if ( rowCol !== null ) {
23332 if ( rowCol.col !== null ){
23333 scrollFocus.colName = rowCol.col.colDef.name;
23334 }
23335 if ( rowCol.row !== null ){
23336 scrollFocus.rowVal = service.getRowVal( grid, rowCol.row );
23337 }
23338 }
23339 }
23340
23341 if ( grid.options.saveScroll || grid.options.saveFocus && !scrollFocus.colName && !scrollFocus.rowVal ) {
23342 scrollFocus.focus = false;
23343 if ( grid.renderContainers.body.prevRowScrollIndex ){
23344 scrollFocus.rowVal = service.getRowVal( grid, grid.renderContainers.body.visibleRowCache[ grid.renderContainers.body.prevRowScrollIndex ]);
23345 }
23346
23347 if ( grid.renderContainers.body.prevColScrollIndex ){
23348 scrollFocus.colName = grid.renderContainers.body.visibleColumnCache[ grid.renderContainers.body.prevColScrollIndex ].name;
23349 }
23350 }
23351
23352 return scrollFocus;
23353 },
23354
23355
23356 /**
23357 * @ngdoc function
23358 * @name saveSelection
23359 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23360 * @description Saves the currently selected rows, if the selection feature is enabled
23361 * @param {Grid} grid the grid whose state we'd like to save
23362 * @returns {array} the selection state ready to be saved
23363 */
23364 saveSelection: function( grid ){
23365 if ( !grid.api.selection || !grid.options.saveSelection ){
23366 return [];
23367 }
23368
23369 var selection = grid.api.selection.getSelectedGridRows().map( function( gridRow ) {
23370 return service.getRowVal( grid, gridRow );
23371 });
23372
23373 return selection;
23374 },
23375
23376
23377 /**
23378 * @ngdoc function
23379 * @name saveGrouping
23380 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23381 * @description Saves the grouping state, if the grouping feature is enabled
23382 * @param {Grid} grid the grid whose state we'd like to save
23383 * @returns {object} the grouping state ready to be saved
23384 */
23385 saveGrouping: function( grid ){
23386 if ( !grid.api.grouping || !grid.options.saveGrouping ){
23387 return {};
23388 }
23389
23390 return grid.api.grouping.getGrouping( grid.options.saveGroupingExpandedStates );
23391 },
23392
23393
23394 /**
23395 * @ngdoc function
23396 * @name saveTreeView
23397 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23398 * @description Saves the tree view state, if the tree feature is enabled
23399 * @param {Grid} grid the grid whose state we'd like to save
23400 * @returns {object} the tree view state ready to be saved
23401 */
23402 saveTreeView: function( grid ){
23403 if ( !grid.api.treeView || !grid.options.saveTreeView ){
23404 return {};
23405 }
23406
23407 return grid.api.treeView.getTreeView();
23408 },
23409
23410
23411 /**
23412 * @ngdoc function
23413 * @name getRowVal
23414 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23415 * @description Helper function that gets either the rowNum or
23416 * the saveRowIdentity, given a gridRow
23417 * @param {Grid} grid the grid the row is in
23418 * @param {GridRow} gridRow the row we want the rowNum for
23419 * @returns {object} an object containing { identity: true/false, row: rowNumber/rowIdentity }
23420 *
23421 */
23422 getRowVal: function( grid, gridRow ){
23423 if ( !gridRow ) {
23424 return null;
23425 }
23426
23427 var rowVal = {};
23428 if ( grid.options.saveRowIdentity ){
23429 rowVal.identity = true;
23430 rowVal.row = grid.options.saveRowIdentity( gridRow.entity );
23431 } else {
23432 rowVal.identity = false;
23433 rowVal.row = grid.renderContainers.body.visibleRowCache.indexOf( gridRow );
23434 }
23435 return rowVal;
23436 },
23437
23438
23439 /**
23440 * @ngdoc function
23441 * @name restoreColumns
23442 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23443 * @description Restores the columns, including order, visible, width,
23444 * pinning, sort and filters.
23445 *
23446 * @param {Grid} grid the grid whose state we'd like to restore
23447 * @param {object} columnsState the list of columns we had before, with their state
23448 */
23449 restoreColumns: function( grid, columnsState ){
23450 var isSortChanged = false;
23451
23452 columnsState.forEach( function( columnState, index ) {
23453 var currentCol = grid.getColumn( columnState.name );
23454
23455 if ( currentCol && !grid.isRowHeaderColumn(currentCol) ){
23456 if ( grid.options.saveVisible &&
23457 ( currentCol.visible !== columnState.visible ||
23458 currentCol.colDef.visible !== columnState.visible ) ){
23459 currentCol.visible = columnState.visible;
23460 currentCol.colDef.visible = columnState.visible;
23461 grid.api.core.raise.columnVisibilityChanged(currentCol);
23462 }
23463
23464 if ( grid.options.saveWidths ){
23465 currentCol.width = columnState.width;
23466 }
23467
23468 if ( grid.options.saveSort &&
23469 !angular.equals(currentCol.sort, columnState.sort) &&
23470 !( currentCol.sort === undefined && angular.isEmpty(columnState.sort) ) ){
23471 currentCol.sort = angular.copy( columnState.sort );
23472 isSortChanged = true;
23473 }
23474
23475 if ( grid.options.saveFilter &&
23476 !angular.equals(currentCol.filters, columnState.filters ) ){
23477 columnState.filters.forEach( function( filter, index ){
23478 angular.extend( currentCol.filters[index], filter );
23479 if ( typeof(filter.term) === 'undefined' || filter.term === null ){
23480 delete currentCol.filters[index].term;
23481 }
23482 });
23483 grid.api.core.raise.filterChanged();
23484 }
23485
23486 if ( !!grid.api.pinning && grid.options.savePinning && currentCol.renderContainer !== columnState.pinned ){
23487 grid.api.pinning.pinColumn(currentCol, columnState.pinned);
23488 }
23489
23490 var currentIndex = grid.getOnlyDataColumns().indexOf( currentCol );
23491 if (currentIndex !== -1) {
23492 if (grid.options.saveOrder && currentIndex !== index) {
23493 var column = grid.columns.splice(currentIndex + grid.rowHeaderColumns.length, 1)[0];
23494 grid.columns.splice(index + grid.rowHeaderColumns.length, 0, column);
23495 }
23496 }
23497 }
23498 });
23499
23500 if ( isSortChanged ) {
23501 grid.api.core.raise.sortChanged( grid, grid.getColumnSorting() );
23502 }
23503 },
23504
23505
23506 /**
23507 * @ngdoc function
23508 * @name restoreScrollFocus
23509 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23510 * @description Scrolls to the position that was saved. If focus is true, then
23511 * sets focus to the specified row/col. If focus is false, then scrolls to the
23512 * specified row/col.
23513 *
23514 * @param {Grid} grid the grid whose state we'd like to restore
23515 * @param {scope} $scope a scope that we can broadcast on
23516 * @param {object} scrollFocusState the scroll/focus state ready to be restored
23517 */
23518 restoreScrollFocus: function( grid, $scope, scrollFocusState ){
23519 if ( !grid.api.cellNav ){
23520 return;
23521 }
23522
23523 var colDef, row;
23524 if ( scrollFocusState.colName ){
23525 var colDefs = grid.options.columnDefs.filter( function( colDef ) { return colDef.name === scrollFocusState.colName; });
23526 if ( colDefs.length > 0 ){
23527 colDef = colDefs[0];
23528 }
23529 }
23530
23531 if ( scrollFocusState.rowVal && scrollFocusState.rowVal.row ){
23532 if ( scrollFocusState.rowVal.identity ){
23533 row = service.findRowByIdentity( grid, scrollFocusState.rowVal );
23534 } else {
23535 row = grid.renderContainers.body.visibleRowCache[ scrollFocusState.rowVal.row ];
23536 }
23537 }
23538
23539 var entity = row && row.entity ? row.entity : null ;
23540
23541 if ( colDef || entity ) {
23542 if (scrollFocusState.focus ){
23543 grid.api.cellNav.scrollToFocus( entity, colDef );
23544 } else {
23545 grid.scrollTo( entity, colDef );
23546 }
23547 }
23548 },
23549
23550
23551 /**
23552 * @ngdoc function
23553 * @name restoreSelection
23554 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23555 * @description Selects the rows that are provided in the selection
23556 * state. If you are using `saveRowIdentity` and more than one row matches the identity
23557 * function then only the first is selected.
23558 * @param {Grid} grid the grid whose state we'd like to restore
23559 * @param {object} selectionState the selection state ready to be restored
23560 */
23561 restoreSelection: function( grid, selectionState ){
23562 if ( !grid.api.selection ){
23563 return;
23564 }
23565
23566 grid.api.selection.clearSelectedRows();
23567
23568 selectionState.forEach( function( rowVal ) {
23569 if ( rowVal.identity ){
23570 var foundRow = service.findRowByIdentity( grid, rowVal );
23571
23572 if ( foundRow ){
23573 grid.api.selection.selectRow( foundRow.entity );
23574 }
23575
23576 } else {
23577 grid.api.selection.selectRowByVisibleIndex( rowVal.row );
23578 }
23579 });
23580 },
23581
23582
23583 /**
23584 * @ngdoc function
23585 * @name restoreGrouping
23586 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23587 * @description Restores the grouping configuration, if the grouping feature
23588 * is enabled.
23589 * @param {Grid} grid the grid whose state we'd like to restore
23590 * @param {object} groupingState the grouping state ready to be restored
23591 */
23592 restoreGrouping: function( grid, groupingState ){
23593 if ( !grid.api.grouping || typeof(groupingState) === 'undefined' || groupingState === null || angular.equals(groupingState, {}) ){
23594 return;
23595 }
23596
23597 grid.api.grouping.setGrouping( groupingState );
23598 },
23599
23600 /**
23601 * @ngdoc function
23602 * @name restoreTreeView
23603 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23604 * @description Restores the tree view configuration, if the tree view feature
23605 * is enabled.
23606 * @param {Grid} grid the grid whose state we'd like to restore
23607 * @param {object} treeViewState the tree view state ready to be restored
23608 */
23609 restoreTreeView: function( grid, treeViewState ){
23610 if ( !grid.api.treeView || typeof(treeViewState) === 'undefined' || treeViewState === null || angular.equals(treeViewState, {}) ){
23611 return;
23612 }
23613
23614 grid.api.treeView.setTreeView( treeViewState );
23615 },
23616
23617 /**
23618 * @ngdoc function
23619 * @name findRowByIdentity
23620 * @methodOf ui.grid.saveState.service:uiGridSaveStateService
23621 * @description Finds a row given it's identity value, returns the first found row
23622 * if any are found, otherwise returns null if no rows are found.
23623 * @param {Grid} grid the grid whose state we'd like to restore
23624 * @param {object} rowVal the row we'd like to find
23625 * @returns {gridRow} the found row, or null if none found
23626 */
23627 findRowByIdentity: function( grid, rowVal ){
23628 if ( !grid.options.saveRowIdentity ){
23629 return null;
23630 }
23631
23632 var filteredRows = grid.rows.filter( function( gridRow ) {
23633 if ( grid.options.saveRowIdentity( gridRow.entity ) === rowVal.row ){
23634 return true;
23635 } else {
23636 return false;
23637 }
23638 });
23639
23640 if ( filteredRows.length > 0 ){
23641 return filteredRows[0];
23642 } else {
23643 return null;
23644 }
23645 }
23646 };
23647
23648 return service;
23649
23650 }
23651 ]);
23652
23653 /**
23654 * @ngdoc directive
23655 * @name ui.grid.saveState.directive:uiGridSaveState
23656 * @element div
23657 * @restrict A
23658 *
23659 * @description Adds saveState features to grid
23660 *
23661 * @example
23662 <example module="app">
23663 <file name="app.js">
23664 var app = angular.module('app', ['ui.grid', 'ui.grid.saveState']);
23665
23666 app.controller('MainCtrl', ['$scope', function ($scope) {
23667 $scope.data = [
23668 { name: 'Bob', title: 'CEO' },
23669 { name: 'Frank', title: 'Lowly Developer' }
23670 ];
23671
23672 $scope.gridOptions = {
23673 columnDefs: [
23674 {name: 'name'},
23675 {name: 'title', enableCellEdit: true}
23676 ],
23677 data: $scope.data
23678 };
23679 }]);
23680 </file>
23681 <file name="index.html">
23682 <div ng-controller="MainCtrl">
23683 <div ui-grid="gridOptions" ui-grid-save-state></div>
23684 </div>
23685 </file>
23686 </example>
23687 */
23688 module.directive('uiGridSaveState', ['uiGridSaveStateConstants', 'uiGridSaveStateService', 'gridUtil', '$compile',
23689 function (uiGridSaveStateConstants, uiGridSaveStateService, gridUtil, $compile) {
23690 return {
23691 replace: true,
23692 priority: 0,
23693 require: '^uiGrid',
23694 scope: false,
23695 link: function ($scope, $elm, $attrs, uiGridCtrl) {
23696 uiGridSaveStateService.initializeGrid(uiGridCtrl.grid);
23697 }
23698 };
23699 }
23700 ]);
23701 })();
23702
23703 (function () {
23704 'use strict';
23705
23706 /**
23707 * @ngdoc overview
23708 * @name ui.grid.selection
23709 * @description
23710 *
23711 * # ui.grid.selection
23712 * This module provides row selection
23713 *
23714 * <div class="alert alert-success" role="alert"><strong>Stable</strong> This feature is stable. There should no longer be breaking api changes without a deprecation warning.</div>
23715 *
23716 * <div doc-module-components="ui.grid.selection"></div>
23717 */
23718
23719 var module = angular.module('ui.grid.selection', ['ui.grid']);
23720
23721 /**
23722 * @ngdoc object
23723 * @name ui.grid.selection.constant:uiGridSelectionConstants
23724 *
23725 * @description constants available in selection module
23726 */
23727 module.constant('uiGridSelectionConstants', {
23728 featureName: "selection",
23729 selectionRowHeaderColName: 'selectionRowHeaderCol'
23730 });
23731
23732 //add methods to GridRow
23733 angular.module('ui.grid').config(['$provide', function($provide) {
23734 $provide.decorator('GridRow', ['$delegate', function($delegate) {
23735
23736 /**
23737 * @ngdoc object
23738 * @name ui.grid.selection.api:GridRow
23739 *
23740 * @description GridRow prototype functions added for selection
23741 */
23742
23743 /**
23744 * @ngdoc object
23745 * @name enableSelection
23746 * @propertyOf ui.grid.selection.api:GridRow
23747 * @description Enable row selection for this row, only settable by internal code.
23748 *
23749 * The grouping feature, for example, might set group header rows to not be selectable.
23750 * <br/>Defaults to true
23751 */
23752
23753 /**
23754 * @ngdoc object
23755 * @name isSelected
23756 * @propertyOf ui.grid.selection.api:GridRow
23757 * @description Selected state of row. Should be readonly. Make any changes to selected state using setSelected().
23758 * <br/>Defaults to false
23759 */
23760
23761
23762 /**
23763 * @ngdoc function
23764 * @name setSelected
23765 * @methodOf ui.grid.selection.api:GridRow
23766 * @description Sets the isSelected property and updates the selectedCount
23767 * Changes to isSelected state should only be made via this function
23768 * @param {bool} selected value to set
23769 */
23770 $delegate.prototype.setSelected = function(selected) {
23771 this.isSelected = selected;
23772 if (selected) {
23773 this.grid.selection.selectedCount++;
23774 }
23775 else {
23776 this.grid.selection.selectedCount--;
23777 }
23778 };
23779
23780 return $delegate;
23781 }]);
23782 }]);
23783
23784 /**
23785 * @ngdoc service
23786 * @name ui.grid.selection.service:uiGridSelectionService
23787 *
23788 * @description Services for selection features
23789 */
23790 module.service('uiGridSelectionService', ['$q', '$templateCache', 'uiGridSelectionConstants', 'gridUtil',
23791 function ($q, $templateCache, uiGridSelectionConstants, gridUtil) {
23792
23793 var service = {
23794
23795 initializeGrid: function (grid) {
23796
23797 //add feature namespace and any properties to grid for needed
23798 /**
23799 * @ngdoc object
23800 * @name ui.grid.selection.grid:selection
23801 *
23802 * @description Grid properties and functions added for selection
23803 */
23804 grid.selection = {};
23805 grid.selection.lastSelectedRow = null;
23806 grid.selection.selectAll = false;
23807
23808
23809 /**
23810 * @ngdoc object
23811 * @name selectedCount
23812 * @propertyOf ui.grid.selection.grid:selection
23813 * @description Current count of selected rows
23814 * @example
23815 * var count = grid.selection.selectedCount
23816 */
23817 grid.selection.selectedCount = 0;
23818
23819 service.defaultGridOptions(grid.options);
23820
23821 /**
23822 * @ngdoc object
23823 * @name ui.grid.selection.api:PublicApi
23824 *
23825 * @description Public Api for selection feature
23826 */
23827 var publicApi = {
23828 events: {
23829 selection: {
23830 /**
23831 * @ngdoc event
23832 * @name rowSelectionChanged
23833 * @eventOf ui.grid.selection.api:PublicApi
23834 * @description is raised after the row.isSelected state is changed
23835 * @param {GridRow} row the row that was selected/deselected
23836 * @param {Event} event object if raised from an event
23837 */
23838 rowSelectionChanged: function (scope, row, evt) {
23839 },
23840 /**
23841 * @ngdoc event
23842 * @name rowSelectionChangedBatch
23843 * @eventOf ui.grid.selection.api:PublicApi
23844 * @description is raised after the row.isSelected state is changed
23845 * in bulk, if the `enableSelectionBatchEvent` option is set to true
23846 * (which it is by default). This allows more efficient processing
23847 * of bulk events.
23848 * @param {array} rows the rows that were selected/deselected
23849 * @param {Event} event object if raised from an event
23850 */
23851 rowSelectionChangedBatch: function (scope, rows, evt) {
23852 }
23853 }
23854 },
23855 methods: {
23856 selection: {
23857 /**
23858 * @ngdoc function
23859 * @name toggleRowSelection
23860 * @methodOf ui.grid.selection.api:PublicApi
23861 * @description Toggles data row as selected or unselected
23862 * @param {object} rowEntity gridOptions.data[] array instance
23863 * @param {Event} event object if raised from an event
23864 */
23865 toggleRowSelection: function (rowEntity, evt) {
23866 var row = grid.getRow(rowEntity);
23867 if (row !== null) {
23868 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23869 }
23870 },
23871 /**
23872 * @ngdoc function
23873 * @name selectRow
23874 * @methodOf ui.grid.selection.api:PublicApi
23875 * @description Select the data row
23876 * @param {object} rowEntity gridOptions.data[] array instance
23877 * @param {Event} event object if raised from an event
23878 */
23879 selectRow: function (rowEntity, evt) {
23880 var row = grid.getRow(rowEntity);
23881 if (row !== null && !row.isSelected) {
23882 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23883 }
23884 },
23885 /**
23886 * @ngdoc function
23887 * @name selectRowByVisibleIndex
23888 * @methodOf ui.grid.selection.api:PublicApi
23889 * @description Select the specified row by visible index (i.e. if you
23890 * specify row 0 you'll get the first visible row selected). In this context
23891 * visible means of those rows that are theoretically visible (i.e. not filtered),
23892 * rather than rows currently rendered on the screen.
23893 * @param {number} index index within the rowsVisible array
23894 * @param {Event} event object if raised from an event
23895 */
23896 selectRowByVisibleIndex: function ( rowNum, evt ) {
23897 var row = grid.renderContainers.body.visibleRowCache[rowNum];
23898 if (row !== null && typeof(row) !== 'undefined' && !row.isSelected) {
23899 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23900 }
23901 },
23902 /**
23903 * @ngdoc function
23904 * @name unSelectRow
23905 * @methodOf ui.grid.selection.api:PublicApi
23906 * @description UnSelect the data row
23907 * @param {object} rowEntity gridOptions.data[] array instance
23908 * @param {Event} event object if raised from an event
23909 */
23910 unSelectRow: function (rowEntity, evt) {
23911 var row = grid.getRow(rowEntity);
23912 if (row !== null && row.isSelected) {
23913 service.toggleRowSelection(grid, row, evt, grid.options.multiSelect, grid.options.noUnselect);
23914 }
23915 },
23916 /**
23917 * @ngdoc function
23918 * @name selectAllRows
23919 * @methodOf ui.grid.selection.api:PublicApi
23920 * @description Selects all rows. Does nothing if multiSelect = false
23921 * @param {Event} event object if raised from an event
23922 */
23923 selectAllRows: function (evt) {
23924 if (grid.options.multiSelect === false) {
23925 return;
23926 }
23927
23928 var changedRows = [];
23929 grid.rows.forEach(function (row) {
23930 if ( !row.isSelected && row.enableSelection !== false ){
23931 row.setSelected(true);
23932 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
23933 }
23934 });
23935 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
23936 grid.selection.selectAll = true;
23937 },
23938 /**
23939 * @ngdoc function
23940 * @name selectAllVisibleRows
23941 * @methodOf ui.grid.selection.api:PublicApi
23942 * @description Selects all visible rows. Does nothing if multiSelect = false
23943 * @param {Event} event object if raised from an event
23944 */
23945 selectAllVisibleRows: function (evt) {
23946 if (grid.options.multiSelect === false) {
23947 return;
23948 }
23949
23950 var changedRows = [];
23951 grid.rows.forEach(function (row) {
23952 if (row.visible) {
23953 if (!row.isSelected && row.enableSelection !== false){
23954 row.setSelected(true);
23955 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
23956 }
23957 } else {
23958 if (row.isSelected){
23959 row.setSelected(false);
23960 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
23961 }
23962 }
23963 });
23964 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
23965 grid.selection.selectAll = true;
23966 },
23967 /**
23968 * @ngdoc function
23969 * @name clearSelectedRows
23970 * @methodOf ui.grid.selection.api:PublicApi
23971 * @description Unselects all rows
23972 * @param {Event} event object if raised from an event
23973 */
23974 clearSelectedRows: function (evt) {
23975 service.clearSelectedRows(grid, evt);
23976 },
23977 /**
23978 * @ngdoc function
23979 * @name getSelectedRows
23980 * @methodOf ui.grid.selection.api:PublicApi
23981 * @description returns all selectedRow's entity references
23982 */
23983 getSelectedRows: function () {
23984 return service.getSelectedRows(grid).map(function (gridRow) {
23985 return gridRow.entity;
23986 });
23987 },
23988 /**
23989 * @ngdoc function
23990 * @name getSelectedGridRows
23991 * @methodOf ui.grid.selection.api:PublicApi
23992 * @description returns all selectedRow's as gridRows
23993 */
23994 getSelectedGridRows: function () {
23995 return service.getSelectedRows(grid);
23996 },
23997 /**
23998 * @ngdoc function
23999 * @name setMultiSelect
24000 * @methodOf ui.grid.selection.api:PublicApi
24001 * @description Sets the current gridOption.multiSelect to true or false
24002 * @param {bool} multiSelect true to allow multiple rows
24003 */
24004 setMultiSelect: function (multiSelect) {
24005 grid.options.multiSelect = multiSelect;
24006 },
24007 /**
24008 * @ngdoc function
24009 * @name setModifierKeysToMultiSelect
24010 * @methodOf ui.grid.selection.api:PublicApi
24011 * @description Sets the current gridOption.modifierKeysToMultiSelect to true or false
24012 * @param {bool} modifierKeysToMultiSelect true to only allow multiple rows when using ctrlKey or shiftKey is used
24013 */
24014 setModifierKeysToMultiSelect: function (modifierKeysToMultiSelect) {
24015 grid.options.modifierKeysToMultiSelect = modifierKeysToMultiSelect;
24016 },
24017 /**
24018 * @ngdoc function
24019 * @name getSelectAllState
24020 * @methodOf ui.grid.selection.api:PublicApi
24021 * @description Returns whether or not the selectAll checkbox is currently ticked. The
24022 * grid doesn't automatically select rows when you add extra data - so when you add data
24023 * you need to explicitly check whether the selectAll is set, and then call setVisible rows
24024 * if it is
24025 */
24026 getSelectAllState: function () {
24027 return grid.selection.selectAll;
24028 }
24029
24030 }
24031 }
24032 };
24033
24034 grid.api.registerEventsFromObject(publicApi.events);
24035
24036 grid.api.registerMethodsFromObject(publicApi.methods);
24037
24038 },
24039
24040 defaultGridOptions: function (gridOptions) {
24041 //default option to true unless it was explicitly set to false
24042 /**
24043 * @ngdoc object
24044 * @name ui.grid.selection.api:GridOptions
24045 *
24046 * @description GridOptions for selection feature, these are available to be
24047 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
24048 */
24049
24050 /**
24051 * @ngdoc object
24052 * @name enableRowSelection
24053 * @propertyOf ui.grid.selection.api:GridOptions
24054 * @description Enable row selection for entire grid.
24055 * <br/>Defaults to true
24056 */
24057 gridOptions.enableRowSelection = gridOptions.enableRowSelection !== false;
24058 /**
24059 * @ngdoc object
24060 * @name multiSelect
24061 * @propertyOf ui.grid.selection.api:GridOptions
24062 * @description Enable multiple row selection for entire grid
24063 * <br/>Defaults to true
24064 */
24065 gridOptions.multiSelect = gridOptions.multiSelect !== false;
24066 /**
24067 * @ngdoc object
24068 * @name noUnselect
24069 * @propertyOf ui.grid.selection.api:GridOptions
24070 * @description Prevent a row from being unselected. Works in conjunction
24071 * with `multiselect = false` and `gridApi.selection.selectRow()` to allow
24072 * you to create a single selection only grid - a row is always selected, you
24073 * can only select different rows, you can't unselect the row.
24074 * <br/>Defaults to false
24075 */
24076 gridOptions.noUnselect = gridOptions.noUnselect === true;
24077 /**
24078 * @ngdoc object
24079 * @name modifierKeysToMultiSelect
24080 * @propertyOf ui.grid.selection.api:GridOptions
24081 * @description Enable multiple row selection only when using the ctrlKey or shiftKey. Requires multiSelect to be true.
24082 * <br/>Defaults to false
24083 */
24084 gridOptions.modifierKeysToMultiSelect = gridOptions.modifierKeysToMultiSelect === true;
24085 /**
24086 * @ngdoc object
24087 * @name enableRowHeaderSelection
24088 * @propertyOf ui.grid.selection.api:GridOptions
24089 * @description Enable a row header to be used for selection
24090 * <br/>Defaults to true
24091 */
24092 gridOptions.enableRowHeaderSelection = gridOptions.enableRowHeaderSelection !== false;
24093 /**
24094 * @ngdoc object
24095 * @name enableFullRowSelection
24096 * @propertyOf ui.grid.selection.api:GridOptions
24097 * @description Enable selection by clicking anywhere on the row. Defaults to
24098 * false if `enableRowHeaderSelection` is true, otherwise defaults to false.
24099 */
24100 if ( typeof(gridOptions.enableFullRowSelection) === 'undefined' ){
24101 gridOptions.enableFullRowSelection = !gridOptions.enableRowHeaderSelection;
24102 }
24103 /**
24104 * @ngdoc object
24105 * @name enableSelectAll
24106 * @propertyOf ui.grid.selection.api:GridOptions
24107 * @description Enable the select all checkbox at the top of the selectionRowHeader
24108 * <br/>Defaults to true
24109 */
24110 gridOptions.enableSelectAll = gridOptions.enableSelectAll !== false;
24111 /**
24112 * @ngdoc object
24113 * @name enableSelectionBatchEvent
24114 * @propertyOf ui.grid.selection.api:GridOptions
24115 * @description If selected rows are changed in bulk, either via the API or
24116 * via the selectAll checkbox, then a separate event is fired. Setting this
24117 * option to false will cause the rowSelectionChanged event to be called multiple times
24118 * instead
24119 * <br/>Defaults to true
24120 */
24121 gridOptions.enableSelectionBatchEvent = gridOptions.enableSelectionBatchEvent !== false;
24122 /**
24123 * @ngdoc object
24124 * @name selectionRowHeaderWidth
24125 * @propertyOf ui.grid.selection.api:GridOptions
24126 * @description can be used to set a custom width for the row header selection column
24127 * <br/>Defaults to 30px
24128 */
24129 gridOptions.selectionRowHeaderWidth = angular.isDefined(gridOptions.selectionRowHeaderWidth) ? gridOptions.selectionRowHeaderWidth : 30;
24130
24131 /**
24132 * @ngdoc object
24133 * @name enableFooterTotalSelected
24134 * @propertyOf ui.grid.selection.api:GridOptions
24135 * @description Shows the total number of selected items in footer if true.
24136 * <br/>Defaults to true.
24137 * <br/>GridOptions.showGridFooter must also be set to true.
24138 */
24139 gridOptions.enableFooterTotalSelected = gridOptions.enableFooterTotalSelected !== false;
24140
24141 /**
24142 * @ngdoc object
24143 * @name isRowSelectable
24144 * @propertyOf ui.grid.selection.api:GridOptions
24145 * @description Makes it possible to specify a method that evaluates for each row and sets its "enableSelection" property.
24146 */
24147
24148 gridOptions.isRowSelectable = angular.isDefined(gridOptions.isRowSelectable) ? gridOptions.isRowSelectable : angular.noop;
24149 },
24150
24151 /**
24152 * @ngdoc function
24153 * @name toggleRowSelection
24154 * @methodOf ui.grid.selection.service:uiGridSelectionService
24155 * @description Toggles row as selected or unselected
24156 * @param {Grid} grid grid object
24157 * @param {GridRow} row row to select or deselect
24158 * @param {Event} event object if resulting from event
24159 * @param {bool} multiSelect if false, only one row at time can be selected
24160 * @param {bool} noUnselect if true then rows cannot be unselected
24161 */
24162 toggleRowSelection: function (grid, row, evt, multiSelect, noUnselect) {
24163 var selected = row.isSelected;
24164
24165 if ( row.enableSelection === false && !selected ){
24166 return;
24167 }
24168
24169 var selectedRows;
24170 if (!multiSelect && !selected) {
24171 service.clearSelectedRows(grid, evt);
24172 } else if (!multiSelect && selected) {
24173 selectedRows = service.getSelectedRows(grid);
24174 if (selectedRows.length > 1) {
24175 selected = false; // Enable reselect of the row
24176 service.clearSelectedRows(grid, evt);
24177 }
24178 }
24179
24180 if (selected && noUnselect){
24181 // don't deselect the row
24182 } else {
24183 row.setSelected(!selected);
24184 if (row.isSelected === true) {
24185 grid.selection.lastSelectedRow = row;
24186 }
24187
24188 selectedRows = service.getSelectedRows(grid);
24189 grid.selection.selectAll = grid.rows.length === selectedRows.length;
24190
24191 grid.api.selection.raise.rowSelectionChanged(row, evt);
24192 }
24193 },
24194 /**
24195 * @ngdoc function
24196 * @name shiftSelect
24197 * @methodOf ui.grid.selection.service:uiGridSelectionService
24198 * @description selects a group of rows from the last selected row using the shift key
24199 * @param {Grid} grid grid object
24200 * @param {GridRow} clicked row
24201 * @param {Event} event object if raised from an event
24202 * @param {bool} multiSelect if false, does nothing this is for multiSelect only
24203 */
24204 shiftSelect: function (grid, row, evt, multiSelect) {
24205 if (!multiSelect) {
24206 return;
24207 }
24208 var selectedRows = service.getSelectedRows(grid);
24209 var fromRow = selectedRows.length > 0 ? grid.renderContainers.body.visibleRowCache.indexOf(grid.selection.lastSelectedRow) : 0;
24210 var toRow = grid.renderContainers.body.visibleRowCache.indexOf(row);
24211 //reverse select direction
24212 if (fromRow > toRow) {
24213 var tmp = fromRow;
24214 fromRow = toRow;
24215 toRow = tmp;
24216 }
24217
24218 var changedRows = [];
24219 for (var i = fromRow; i <= toRow; i++) {
24220 var rowToSelect = grid.renderContainers.body.visibleRowCache[i];
24221 if (rowToSelect) {
24222 if ( !rowToSelect.isSelected && rowToSelect.enableSelection !== false ){
24223 rowToSelect.setSelected(true);
24224 grid.selection.lastSelectedRow = rowToSelect;
24225 service.decideRaiseSelectionEvent( grid, rowToSelect, changedRows, evt );
24226 }
24227 }
24228 }
24229 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24230 },
24231 /**
24232 * @ngdoc function
24233 * @name getSelectedRows
24234 * @methodOf ui.grid.selection.service:uiGridSelectionService
24235 * @description Returns all the selected rows
24236 * @param {Grid} grid grid object
24237 */
24238 getSelectedRows: function (grid) {
24239 return grid.rows.filter(function (row) {
24240 return row.isSelected;
24241 });
24242 },
24243
24244 /**
24245 * @ngdoc function
24246 * @name clearSelectedRows
24247 * @methodOf ui.grid.selection.service:uiGridSelectionService
24248 * @description Clears all selected rows
24249 * @param {Grid} grid grid object
24250 * @param {Event} event object if raised from an event
24251 */
24252 clearSelectedRows: function (grid, evt) {
24253 var changedRows = [];
24254 service.getSelectedRows(grid).forEach(function (row) {
24255 if ( row.isSelected ){
24256 row.setSelected(false);
24257 service.decideRaiseSelectionEvent( grid, row, changedRows, evt );
24258 }
24259 });
24260 service.decideRaiseSelectionBatchEvent( grid, changedRows, evt );
24261 grid.selection.selectAll = false;
24262 grid.selection.selectedCount = 0;
24263 },
24264
24265 /**
24266 * @ngdoc function
24267 * @name decideRaiseSelectionEvent
24268 * @methodOf ui.grid.selection.service:uiGridSelectionService
24269 * @description Decides whether to raise a single event or a batch event
24270 * @param {Grid} grid grid object
24271 * @param {GridRow} row row that has changed
24272 * @param {array} changedRows an array to which we can append the changed
24273 * @param {Event} event object if raised from an event
24274 * row if we're doing batch events
24275 */
24276 decideRaiseSelectionEvent: function( grid, row, changedRows, evt ){
24277 if ( !grid.options.enableSelectionBatchEvent ){
24278 grid.api.selection.raise.rowSelectionChanged(row, evt);
24279 } else {
24280 changedRows.push(row);
24281 }
24282 },
24283
24284 /**
24285 * @ngdoc function
24286 * @name raiseSelectionEvent
24287 * @methodOf ui.grid.selection.service:uiGridSelectionService
24288 * @description Decides whether we need to raise a batch event, and
24289 * raises it if we do.
24290 * @param {Grid} grid grid object
24291 * @param {array} changedRows an array of changed rows, only populated
24292 * @param {Event} event object if raised from an event
24293 * if we're doing batch events
24294 */
24295 decideRaiseSelectionBatchEvent: function( grid, changedRows, evt ){
24296 if ( changedRows.length > 0 ){
24297 grid.api.selection.raise.rowSelectionChangedBatch(changedRows, evt);
24298 }
24299 }
24300 };
24301
24302 return service;
24303
24304 }]);
24305
24306 /**
24307 * @ngdoc directive
24308 * @name ui.grid.selection.directive:uiGridSelection
24309 * @element div
24310 * @restrict A
24311 *
24312 * @description Adds selection features to grid
24313 *
24314 * @example
24315 <example module="app">
24316 <file name="app.js">
24317 var app = angular.module('app', ['ui.grid', 'ui.grid.selection']);
24318
24319 app.controller('MainCtrl', ['$scope', function ($scope) {
24320 $scope.data = [
24321 { name: 'Bob', title: 'CEO' },
24322 { name: 'Frank', title: 'Lowly Developer' }
24323 ];
24324
24325 $scope.columnDefs = [
24326 {name: 'name', enableCellEdit: true},
24327 {name: 'title', enableCellEdit: true}
24328 ];
24329 }]);
24330 </file>
24331 <file name="index.html">
24332 <div ng-controller="MainCtrl">
24333 <div ui-grid="{ data: data, columnDefs: columnDefs }" ui-grid-selection></div>
24334 </div>
24335 </file>
24336 </example>
24337 */
24338 module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', 'uiGridConstants',
24339 function (uiGridSelectionConstants, uiGridSelectionService, $templateCache, uiGridConstants) {
24340 return {
24341 replace: true,
24342 priority: 0,
24343 require: '^uiGrid',
24344 scope: false,
24345 compile: function () {
24346 return {
24347 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
24348 uiGridSelectionService.initializeGrid(uiGridCtrl.grid);
24349 if (uiGridCtrl.grid.options.enableRowHeaderSelection) {
24350 var selectionRowHeaderDef = {
24351 name: uiGridSelectionConstants.selectionRowHeaderColName,
24352 displayName: '',
24353 width: uiGridCtrl.grid.options.selectionRowHeaderWidth,
24354 minWidth: 10,
24355 cellTemplate: 'ui-grid/selectionRowHeader',
24356 headerCellTemplate: 'ui-grid/selectionHeaderCell',
24357 enableColumnResizing: false,
24358 enableColumnMenu: false,
24359 exporterSuppressExport: true,
24360 allowCellFocus: true
24361 };
24362
24363 uiGridCtrl.grid.addRowHeaderColumn(selectionRowHeaderDef);
24364 }
24365
24366 var processorSet = false;
24367
24368 var processSelectableRows = function( rows ){
24369 rows.forEach(function(row){
24370 row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row);
24371 });
24372 return rows;
24373 };
24374
24375 var updateOptions = function(){
24376 if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) {
24377 uiGridCtrl.grid.registerRowsProcessor(processSelectableRows, 500);
24378 processorSet = true;
24379 }
24380 };
24381
24382 updateOptions();
24383
24384 var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback( updateOptions, [uiGridConstants.dataChange.OPTIONS] );
24385
24386 $scope.$on( '$destroy', dataChangeDereg);
24387 },
24388 post: function ($scope, $elm, $attrs, uiGridCtrl) {
24389
24390 }
24391 };
24392 }
24393 };
24394 }]);
24395
24396 module.directive('uiGridSelectionRowHeaderButtons', ['$templateCache', 'uiGridSelectionService', 'gridUtil',
24397 function ($templateCache, uiGridSelectionService, gridUtil) {
24398 return {
24399 replace: true,
24400 restrict: 'E',
24401 template: $templateCache.get('ui-grid/selectionRowHeaderButtons'),
24402 scope: true,
24403 require: '^uiGrid',
24404 link: function($scope, $elm, $attrs, uiGridCtrl) {
24405 var self = uiGridCtrl.grid;
24406 $scope.selectButtonClick = selectButtonClick;
24407
24408 // On IE, prevent mousedowns on the select button from starting a selection.
24409 // If this is not done and you shift+click on another row, the browser will select a big chunk of text
24410 if (gridUtil.detectBrowser() === 'ie') {
24411 $elm.on('mousedown', selectButtonMouseDown);
24412 }
24413
24414
24415 function selectButtonClick(row, evt) {
24416 evt.stopPropagation();
24417
24418 if (evt.shiftKey) {
24419 uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect);
24420 }
24421 else if (evt.ctrlKey || evt.metaKey) {
24422 uiGridSelectionService.toggleRowSelection(self, row, evt, self.options.multiSelect, self.options.noUnselect);
24423 }
24424 else {
24425 uiGridSelectionService.toggleRowSelection(self, row, evt, (self.options.multiSelect && !self.options.modifierKeysToMultiSelect), self.options.noUnselect);
24426 }
24427 }
24428
24429 function selectButtonMouseDown(evt) {
24430 if (evt.ctrlKey || evt.shiftKey) {
24431 evt.target.onselectstart = function () { return false; };
24432 window.setTimeout(function () { evt.target.onselectstart = null; }, 0);
24433 }
24434 }
24435 }
24436 };
24437 }]);
24438
24439 module.directive('uiGridSelectionSelectAllButtons', ['$templateCache', 'uiGridSelectionService',
24440 function ($templateCache, uiGridSelectionService) {
24441 return {
24442 replace: true,
24443 restrict: 'E',
24444 template: $templateCache.get('ui-grid/selectionSelectAllButtons'),
24445 scope: false,
24446 link: function($scope, $elm, $attrs, uiGridCtrl) {
24447 var self = $scope.col.grid;
24448
24449 $scope.headerButtonClick = function(row, evt) {
24450 if ( self.selection.selectAll ){
24451 uiGridSelectionService.clearSelectedRows(self, evt);
24452 if ( self.options.noUnselect ){
24453 self.api.selection.selectRowByVisibleIndex(0, evt);
24454 }
24455 self.selection.selectAll = false;
24456 } else {
24457 if ( self.options.multiSelect ){
24458 self.api.selection.selectAllVisibleRows(evt);
24459 self.selection.selectAll = true;
24460 }
24461 }
24462 };
24463 }
24464 };
24465 }]);
24466
24467 /**
24468 * @ngdoc directive
24469 * @name ui.grid.selection.directive:uiGridViewport
24470 * @element div
24471 *
24472 * @description Stacks on top of ui.grid.uiGridViewport to alter the attributes used
24473 * for the grid row
24474 */
24475 module.directive('uiGridViewport',
24476 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService',
24477 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService) {
24478 return {
24479 priority: -200, // run after default directive
24480 scope: false,
24481 compile: function ($elm, $attrs) {
24482 var rowRepeatDiv = angular.element($elm.children().children()[0]);
24483
24484 var existingNgClass = rowRepeatDiv.attr("ng-class");
24485 var newNgClass = '';
24486 if ( existingNgClass ) {
24487 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-row-selected': row.isSelected}";
24488 } else {
24489 newNgClass = "{'ui-grid-row-selected': row.isSelected}";
24490 }
24491 rowRepeatDiv.attr("ng-class", newNgClass);
24492
24493 return {
24494 pre: function ($scope, $elm, $attrs, controllers) {
24495
24496 },
24497 post: function ($scope, $elm, $attrs, controllers) {
24498 }
24499 };
24500 }
24501 };
24502 }]);
24503
24504 /**
24505 * @ngdoc directive
24506 * @name ui.grid.selection.directive:uiGridCell
24507 * @element div
24508 * @restrict A
24509 *
24510 * @description Stacks on top of ui.grid.uiGridCell to provide selection feature
24511 */
24512 module.directive('uiGridCell',
24513 ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', '$timeout',
24514 function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService, $timeout) {
24515 return {
24516 priority: -200, // run after default uiGridCell directive
24517 restrict: 'A',
24518 require: '?^uiGrid',
24519 scope: false,
24520 link: function ($scope, $elm, $attrs, uiGridCtrl) {
24521
24522 var touchStartTime = 0;
24523 var touchTimeout = 300;
24524
24525 // Bind to keydown events in the render container
24526 if (uiGridCtrl.grid.api.cellNav) {
24527
24528 uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) {
24529 if (rowCol === null ||
24530 rowCol.row !== $scope.row ||
24531 rowCol.col !== $scope.col) {
24532 return;
24533 }
24534
24535 if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
24536 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
24537 $scope.$apply();
24538 }
24539
24540 // uiGridCellNavService.scrollToIfNecessary(uiGridCtrl.grid, rowCol.row, rowCol.col);
24541 });
24542 }
24543
24544 //$elm.bind('keydown', function (evt) {
24545 // if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") {
24546 // uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
24547 // $scope.$apply();
24548 // }
24549 //});
24550
24551 var selectCells = function(evt){
24552 // if we get a click, then stop listening for touchend
24553 $elm.off('touchend', touchEnd);
24554
24555 if (evt.shiftKey) {
24556 uiGridSelectionService.shiftSelect($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect);
24557 }
24558 else if (evt.ctrlKey || evt.metaKey) {
24559 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect, $scope.grid.options.noUnselect);
24560 }
24561 else {
24562 uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect);
24563 }
24564 $scope.$apply();
24565
24566 // don't re-enable the touchend handler for a little while - some devices generate both, and it will
24567 // take a little while to move your hand from the mouse to the screen if you have both modes of input
24568 $timeout(function() {
24569 $elm.on('touchend', touchEnd);
24570 }, touchTimeout);
24571 };
24572
24573 var touchStart = function(evt){
24574 touchStartTime = (new Date()).getTime();
24575
24576 // if we get a touch event, then stop listening for click
24577 $elm.off('click', selectCells);
24578 };
24579
24580 var touchEnd = function(evt) {
24581 var touchEndTime = (new Date()).getTime();
24582 var touchTime = touchEndTime - touchStartTime;
24583
24584 if (touchTime < touchTimeout ) {
24585 // short touch
24586 selectCells(evt);
24587 }
24588
24589 // don't re-enable the click handler for a little while - some devices generate both, and it will
24590 // take a little while to move your hand from the screen to the mouse if you have both modes of input
24591 $timeout(function() {
24592 $elm.on('click', selectCells);
24593 }, touchTimeout);
24594 };
24595
24596 function registerRowSelectionEvents() {
24597 if ($scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection) {
24598 $elm.addClass('ui-grid-disable-selection');
24599 $elm.on('touchstart', touchStart);
24600 $elm.on('touchend', touchEnd);
24601 $elm.on('click', selectCells);
24602
24603 $scope.registered = true;
24604 }
24605 }
24606
24607 function deregisterRowSelectionEvents() {
24608 if ($scope.registered){
24609 $elm.removeClass('ui-grid-disable-selection');
24610
24611 $elm.off('touchstart', touchStart);
24612 $elm.off('touchend', touchEnd);
24613 $elm.off('click', selectCells);
24614
24615 $scope.registered = false;
24616 }
24617 }
24618
24619 registerRowSelectionEvents();
24620 // register a dataChange callback so that we can change the selection configuration dynamically
24621 // if the user changes the options
24622 var dataChangeDereg = $scope.grid.registerDataChangeCallback( function() {
24623 if ( $scope.grid.options.enableRowSelection && $scope.grid.options.enableFullRowSelection &&
24624 !$scope.registered ){
24625 registerRowSelectionEvents();
24626 } else if ( ( !$scope.grid.options.enableRowSelection || !$scope.grid.options.enableFullRowSelection ) &&
24627 $scope.registered ){
24628 deregisterRowSelectionEvents();
24629 }
24630 }, [uiGridConstants.dataChange.OPTIONS] );
24631
24632 $elm.on( '$destroy', dataChangeDereg);
24633 }
24634 };
24635 }]);
24636
24637 module.directive('uiGridGridFooter', ['$compile', 'uiGridConstants', 'gridUtil', function ($compile, uiGridConstants, gridUtil) {
24638 return {
24639 restrict: 'EA',
24640 replace: true,
24641 priority: -1000,
24642 require: '^uiGrid',
24643 scope: true,
24644 compile: function ($elm, $attrs) {
24645 return {
24646 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
24647
24648 if (!uiGridCtrl.grid.options.showGridFooter) {
24649 return;
24650 }
24651
24652
24653 gridUtil.getTemplate('ui-grid/gridFooterSelectedItems')
24654 .then(function (contents) {
24655 var template = angular.element(contents);
24656
24657 var newElm = $compile(template)($scope);
24658
24659 angular.element($elm[0].getElementsByClassName('ui-grid-grid-footer')[0]).append(newElm);
24660 });
24661 },
24662
24663 post: function ($scope, $elm, $attrs, controllers) {
24664
24665 }
24666 };
24667 }
24668 };
24669 }]);
24670
24671 })();
24672
24673 (function () {
24674 'use strict';
24675
24676 /**
24677 * @ngdoc overview
24678 * @name ui.grid.treeBase
24679 * @description
24680 *
24681 * # ui.grid.treeBase
24682 *
24683 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
24684 *
24685 * This module provides base tree handling functions that are shared by other features, notably grouping
24686 * and treeView. It provides a tree view of the data, with nodes in that
24687 * tree and leaves.
24688 *
24689 * Design information:
24690 * -------------------
24691 *
24692 * The raw data that is provided must come with a $$treeLevel on any non-leaf node. Grouping will create
24693 * these on all the group header rows, treeView will expect these to be set in the raw data by the user.
24694 * TreeBase will run a rowsProcessor that:
24695 * - builds `treeBase.tree` out of the provided rows
24696 * - permits a recursive sort of the tree
24697 * - maintains the expand/collapse state of each node
24698 * - provides the expand/collapse all button and the expand/collapse buttons
24699 * - maintains the count of children for each node
24700 *
24701 * Each row is updated with a link to the tree node that represents it. Refer {@link ui.grid.treeBase.grid:treeBase.tree tree documentation}
24702 * for information.
24703 *
24704 * TreeBase adds information to the rows
24705 * - treeLevel: if present and > -1 tells us the level (level 0 is the top level)
24706 * - treeNode: pointer to the node in the grid.treeBase.tree that refers
24707 * to this row, allowing us to manipulate the state
24708 *
24709 * Since the logic is baked into the rowsProcessors, it should get triggered whenever
24710 * row order or filtering or anything like that is changed. We recall the expanded state
24711 * across invocations of the rowsProcessors by the reference to the treeNode on the individual
24712 * rows. We rebuild the tree itself quite frequently, when we do this we use the saved treeNodes to
24713 * get the state, but we overwrite the other data in that treeNode.
24714 *
24715 * By default rows are collapsed, which means all data rows have their visible property
24716 * set to false, and only level 0 group rows are set to visible.
24717 *
24718 * We rely on the rowsProcessors to do the actual expanding and collapsing, so we set the flags we want into
24719 * grid.treeBase.tree, then call refresh. This is because we can't easily change the visible
24720 * row cache without calling the processors, and once we've built the logic into the rowProcessors we may as
24721 * well use it all the time.
24722 *
24723 * Tree base provides sorting (on non-grouped columns).
24724 *
24725 * Sorting works in two passes. The standard sorting is performed for any columns that are important to building
24726 * the tree (for example, any grouped columns). Then after the tree is built, a recursive tree sort is performed
24727 * for the remaining sort columns (including the original sort) - these columns are sorted within each tree level
24728 * (so all the level 1 nodes are sorted, then all the level 2 nodes within each level 1 node etc).
24729 *
24730 * To achieve this we make use of the `ignoreSort` property on the sort configuration. The parent feature (treeView or grouping)
24731 * must provide a rowsProcessor that runs with very low priority (typically in the 60-65 range), and that sets
24732 * the `ignoreSort`on any sort that it wants to run on the tree. TreeBase will clear the ignoreSort on all sorts - so it
24733 * will turn on any sorts that haven't run. It will then call a recursive sort on the tree.
24734 *
24735 * Tree base provides treeAggregation. It checks the treeAggregation configuration on each column, and aggregates based on
24736 * the logic provided as it builds the tree. Footer aggregation from the uiGrid core should not be used with treeBase aggregation,
24737 * since it operates on all visible rows, as opposed to to leaf nodes only. Setting `showColumnFooter: true` will show the
24738 * treeAggregations in the column footer. Aggregation information will be collected in the format:
24739 *
24740 * ```
24741 * {
24742 * type: 'count',
24743 * value: 4,
24744 * label: 'count: ',
24745 * rendered: 'count: 4'
24746 * }
24747 * ```
24748 *
24749 * A callback is provided to format the value once it is finalised (aka a valueFilter).
24750 *
24751 * <br/>
24752 * <br/>
24753 *
24754 * <div doc-module-components="ui.grid.treeBase"></div>
24755 */
24756
24757 var module = angular.module('ui.grid.treeBase', ['ui.grid']);
24758
24759 /**
24760 * @ngdoc object
24761 * @name ui.grid.treeBase.constant:uiGridTreeBaseConstants
24762 *
24763 * @description constants available in treeBase module.
24764 *
24765 * These constants are manually copied into grouping and treeView,
24766 * as I haven't found a way to simply include them, and it's not worth
24767 * investing time in for something that changes very infrequently.
24768 *
24769 */
24770 module.constant('uiGridTreeBaseConstants', {
24771 featureName: "treeBase",
24772 rowHeaderColName: 'treeBaseRowHeaderCol',
24773 EXPANDED: 'expanded',
24774 COLLAPSED: 'collapsed',
24775 aggregation: {
24776 COUNT: 'count',
24777 SUM: 'sum',
24778 MAX: 'max',
24779 MIN: 'min',
24780 AVG: 'avg'
24781 }
24782 });
24783
24784 /**
24785 * @ngdoc service
24786 * @name ui.grid.treeBase.service:uiGridTreeBaseService
24787 *
24788 * @description Services for treeBase feature
24789 */
24790 /**
24791 * @ngdoc object
24792 * @name ui.grid.treeBase.api:ColumnDef
24793 *
24794 * @description ColumnDef for tree feature, these are available to be
24795 * set using the ui-grid {@link ui.grid.class:GridOptions.columnDef gridOptions.columnDefs}
24796 */
24797
24798 module.service('uiGridTreeBaseService', ['$q', 'uiGridTreeBaseConstants', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', 'rowSorter',
24799 function ($q, uiGridTreeBaseConstants, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants, rowSorter) {
24800
24801 var service = {
24802
24803 initializeGrid: function (grid, $scope) {
24804
24805 //add feature namespace and any properties to grid for needed
24806 /**
24807 * @ngdoc object
24808 * @name ui.grid.treeBase.grid:treeBase
24809 *
24810 * @description Grid properties and functions added for treeBase
24811 */
24812 grid.treeBase = {};
24813
24814 /**
24815 * @ngdoc property
24816 * @propertyOf ui.grid.treeBase.grid:treeBase
24817 * @name numberLevels
24818 *
24819 * @description Total number of tree levels currently used, calculated by the rowsProcessor by
24820 * retaining the highest tree level it sees
24821 */
24822 grid.treeBase.numberLevels = 0;
24823
24824 /**
24825 * @ngdoc property
24826 * @propertyOf ui.grid.treeBase.grid:treeBase
24827 * @name expandAll
24828 *
24829 * @description Whether or not the expandAll box is selected
24830 */
24831 grid.treeBase.expandAll = false;
24832
24833 /**
24834 * @ngdoc property
24835 * @propertyOf ui.grid.treeBase.grid:treeBase
24836 * @name tree
24837 *
24838 * @description Tree represented as a nested array that holds the state of each node, along with a
24839 * pointer to the row. The array order is material - we will display the children in the order
24840 * they are stored in the array
24841 *
24842 * Each node stores:
24843 *
24844 * - the state of this node
24845 * - an array of children of this node
24846 * - a pointer to the parent of this node (reverse pointer, allowing us to walk up the tree)
24847 * - the number of children of this node
24848 * - aggregation information calculated from the nodes
24849 *
24850 * ```
24851 * [{
24852 * state: 'expanded',
24853 * row: <reference to row>,
24854 * parentRow: null,
24855 * aggregations: [{
24856 * type: 'count',
24857 * col: <gridCol>,
24858 * value: 2,
24859 * label: 'count: ',
24860 * rendered: 'count: 2'
24861 * }],
24862 * children: [
24863 * {
24864 * state: 'expanded',
24865 * row: <reference to row>,
24866 * parentRow: <reference to row>,
24867 * aggregations: [{
24868 * type: 'count',
24869 * col: '<gridCol>,
24870 * value: 4,
24871 * label: 'count: ',
24872 * rendered: 'count: 4'
24873 * }],
24874 * children: [
24875 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
24876 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
24877 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
24878 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> }
24879 * ]
24880 * },
24881 * {
24882 * state: 'collapsed',
24883 * row: <reference to row>,
24884 * parentRow: <reference to row>,
24885 * aggregations: [{
24886 * type: 'count',
24887 * col: <gridCol>,
24888 * value: 3,
24889 * label: 'count: ',
24890 * rendered: 'count: 3'
24891 * }],
24892 * children: [
24893 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> },
24894 * { state: 'collapsed', row: <reference to row>, parentRow: <reference to row> },
24895 * { state: 'expanded', row: <reference to row>, parentRow: <reference to row> }
24896 * ]
24897 * }
24898 * ]
24899 * }, {<another level 0 node maybe>} ]
24900 * ```
24901 * Missing state values are false - meaning they aren't expanded.
24902 *
24903 * This is used because the rowProcessors run every time the grid is refreshed, so
24904 * we'd lose the expanded state every time the grid was refreshed. This instead gives
24905 * us a reliable lookup that persists across rowProcessors.
24906 *
24907 * This tree is rebuilt every time we run the rowsProcessors. Since each row holds a pointer
24908 * to it's tree node we can persist expand/collapse state across calls to rowsProcessor, we discard
24909 * all transient information on the tree (children, childCount) and recalculate it
24910 *
24911 */
24912 grid.treeBase.tree = {};
24913
24914 service.defaultGridOptions(grid.options);
24915
24916 grid.registerRowsProcessor(service.treeRows, 410);
24917
24918 grid.registerColumnBuilder( service.treeBaseColumnBuilder );
24919
24920 service.createRowHeader( grid );
24921
24922 /**
24923 * @ngdoc object
24924 * @name ui.grid.treeBase.api:PublicApi
24925 *
24926 * @description Public Api for treeBase feature
24927 */
24928 var publicApi = {
24929 events: {
24930 treeBase: {
24931 /**
24932 * @ngdoc event
24933 * @eventOf ui.grid.treeBase.api:PublicApi
24934 * @name rowExpanded
24935 * @description raised whenever a row is expanded. If you are dynamically
24936 * rendering your tree you can listen to this event, and then retrieve
24937 * the children of this row and load them into the grid data.
24938 *
24939 * When the data is loaded the grid will automatically refresh to show these new rows
24940 *
24941 * <pre>
24942 * gridApi.treeBase.on.rowExpanded(scope,function(row){})
24943 * </pre>
24944 * @param {gridRow} row the row that was expanded. You can also
24945 * retrieve the grid from this row with row.grid
24946 */
24947 rowExpanded: {},
24948
24949 /**
24950 * @ngdoc event
24951 * @eventOf ui.grid.treeBase.api:PublicApi
24952 * @name rowCollapsed
24953 * @description raised whenever a row is collapsed. Doesn't really have
24954 * a purpose at the moment, included for symmetry
24955 *
24956 * <pre>
24957 * gridApi.treeBase.on.rowCollapsed(scope,function(row){})
24958 * </pre>
24959 * @param {gridRow} row the row that was collapsed. You can also
24960 * retrieve the grid from this row with row.grid
24961 */
24962 rowCollapsed: {}
24963 }
24964 },
24965
24966 methods: {
24967 treeBase: {
24968 /**
24969 * @ngdoc function
24970 * @name expandAllRows
24971 * @methodOf ui.grid.treeBase.api:PublicApi
24972 * @description Expands all tree rows
24973 */
24974 expandAllRows: function () {
24975 service.expandAllRows(grid);
24976 },
24977
24978 /**
24979 * @ngdoc function
24980 * @name collapseAllRows
24981 * @methodOf ui.grid.treeBase.api:PublicApi
24982 * @description collapse all tree rows
24983 */
24984 collapseAllRows: function () {
24985 service.collapseAllRows(grid);
24986 },
24987
24988 /**
24989 * @ngdoc function
24990 * @name toggleRowTreeState
24991 * @methodOf ui.grid.treeBase.api:PublicApi
24992 * @description call expand if the row is collapsed, collapse if it is expanded
24993 * @param {gridRow} row the row you wish to toggle
24994 */
24995 toggleRowTreeState: function (row) {
24996 service.toggleRowTreeState(grid, row);
24997 },
24998
24999 /**
25000 * @ngdoc function
25001 * @name expandRow
25002 * @methodOf ui.grid.treeBase.api:PublicApi
25003 * @description expand the immediate children of the specified row
25004 * @param {gridRow} row the row you wish to expand
25005 */
25006 expandRow: function (row) {
25007 service.expandRow(grid, row);
25008 },
25009
25010 /**
25011 * @ngdoc function
25012 * @name expandRowChildren
25013 * @methodOf ui.grid.treeBase.api:PublicApi
25014 * @description expand all children of the specified row
25015 * @param {gridRow} row the row you wish to expand
25016 */
25017 expandRowChildren: function (row) {
25018 service.expandRowChildren(grid, row);
25019 },
25020
25021 /**
25022 * @ngdoc function
25023 * @name collapseRow
25024 * @methodOf ui.grid.treeBase.api:PublicApi
25025 * @description collapse the specified row. When
25026 * you expand the row again, all grandchildren will retain their state
25027 * @param {gridRow} row the row you wish to collapse
25028 */
25029 collapseRow: function ( row ) {
25030 service.collapseRow(grid, row);
25031 },
25032
25033 /**
25034 * @ngdoc function
25035 * @name collapseRowChildren
25036 * @methodOf ui.grid.treeBase.api:PublicApi
25037 * @description collapse all children of the specified row. When
25038 * you expand the row again, all grandchildren will be collapsed
25039 * @param {gridRow} row the row you wish to collapse children for
25040 */
25041 collapseRowChildren: function ( row ) {
25042 service.collapseRowChildren(grid, row);
25043 },
25044
25045 /**
25046 * @ngdoc function
25047 * @name getTreeState
25048 * @methodOf ui.grid.treeBase.api:PublicApi
25049 * @description Get the tree state for this grid,
25050 * used by the saveState feature
25051 * Returned treeState as an object
25052 * `{ expandedState: { uid: 'expanded', uid: 'collapsed' } }`
25053 * where expandedState is a hash of row uid and the current expanded state
25054 *
25055 * @returns {object} tree state
25056 *
25057 * TODO - this needs work - we need an identifier that persists across instantiations,
25058 * not uid. This really means we need a row identity defined, but that won't work for
25059 * grouping. Perhaps this needs to be moved up to treeView and grouping, rather than
25060 * being in base.
25061 */
25062 getTreeExpandedState: function () {
25063 return { expandedState: service.getTreeState(grid) };
25064 },
25065
25066 /**
25067 * @ngdoc function
25068 * @name setTreeState
25069 * @methodOf ui.grid.treeBase.api:PublicApi
25070 * @description Set the expanded states of the tree
25071 * @param {object} config the config you want to apply, in the format
25072 * provided by getTreeState
25073 */
25074 setTreeState: function ( config ) {
25075 service.setTreeState( grid, config );
25076 },
25077
25078 /**
25079 * @ngdoc function
25080 * @name getRowChildren
25081 * @methodOf ui.grid.treeBase.api:PublicApi
25082 * @description Get the children of the specified row
25083 * @param {GridRow} row the row you want the children of
25084 * @returns {Array} array of children of this row, the children
25085 * are all gridRows
25086 */
25087 getRowChildren: function ( row ){
25088 return row.treeNode.children.map( function( childNode ){
25089 return childNode.row;
25090 });
25091 }
25092 }
25093 }
25094 };
25095
25096 grid.api.registerEventsFromObject(publicApi.events);
25097
25098 grid.api.registerMethodsFromObject(publicApi.methods);
25099 },
25100
25101
25102 defaultGridOptions: function (gridOptions) {
25103 //default option to true unless it was explicitly set to false
25104 /**
25105 * @ngdoc object
25106 * @name ui.grid.treeBase.api:GridOptions
25107 *
25108 * @description GridOptions for treeBase feature, these are available to be
25109 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
25110 */
25111
25112 /**
25113 * @ngdoc object
25114 * @name treeRowHeaderBaseWidth
25115 * @propertyOf ui.grid.treeBase.api:GridOptions
25116 * @description Base width of the tree header, provides for a single level of tree. This
25117 * is incremented by `treeIndent` for each extra level
25118 * <br/>Defaults to 30
25119 */
25120 gridOptions.treeRowHeaderBaseWidth = gridOptions.treeRowHeaderBaseWidth || 30;
25121
25122 /**
25123 * @ngdoc object
25124 * @name treeIndent
25125 * @propertyOf ui.grid.treeBase.api:GridOptions
25126 * @description Number of pixels of indent for the icon at each tree level, wider indents are visually more pleasing,
25127 * but will make the tree row header wider
25128 * <br/>Defaults to 10
25129 */
25130 gridOptions.treeIndent = gridOptions.treeIndent || 10;
25131
25132 /**
25133 * @ngdoc object
25134 * @name showTreeRowHeader
25135 * @propertyOf ui.grid.treeBase.api:GridOptions
25136 * @description If set to false, don't create the row header. Youll need to programatically control the expand
25137 * states
25138 * <br/>Defaults to true
25139 */
25140 gridOptions.showTreeRowHeader = gridOptions.showTreeRowHeader !== false;
25141
25142 /**
25143 * @ngdoc object
25144 * @name showTreeExpandNoChildren
25145 * @propertyOf ui.grid.treeBase.api:GridOptions
25146 * @description If set to true, show the expand/collapse button even if there are no
25147 * children of a node. You'd use this if you're planning to dynamically load the children
25148 *
25149 * <br/>Defaults to true, grouping overrides to false
25150 */
25151 gridOptions.showTreeExpandNoChildren = gridOptions.showTreeExpandNoChildren !== false;
25152
25153 /**
25154 * @ngdoc object
25155 * @name treeRowHeaderAlwaysVisible
25156 * @propertyOf ui.grid.treeBase.api:GridOptions
25157 * @description If set to true, row header even if there are no tree nodes
25158 *
25159 * <br/>Defaults to true
25160 */
25161 gridOptions.treeRowHeaderAlwaysVisible = gridOptions.treeRowHeaderAlwaysVisible !== false;
25162
25163 /**
25164 * @ngdoc object
25165 * @name treeCustomAggregations
25166 * @propertyOf ui.grid.treeBase.api:GridOptions
25167 * @description Define custom aggregation functions. The properties of this object will be
25168 * aggregation types available for use on columnDef with {@link ui.grid.treeBase.api:ColumnDef treeAggregationType} or through the column menu.
25169 * If a function defined here uses the same name as one of the native aggregations, this one will take precedence.
25170 * The object format is:
25171 *
25172 * <pre>
25173 * {
25174 * aggregationName: {
25175 * label: (optional) string,
25176 * aggregationFn: function( aggregation, fieldValue, numValue, row ){...},
25177 * finalizerFn: (optional) function( aggregation ){...}
25178 * },
25179 * mean: {
25180 * label: 'mean',
25181 * aggregationFn: function( aggregation, fieldValue, numValue ){
25182 * aggregation.count = (aggregation.count || 1) + 1;
25183 * aggregation.sum = (aggregation.sum || 0) + numValue;
25184 * },
25185 * finalizerFn: function( aggregation ){
25186 * aggregation.value = aggregation.sum / aggregation.count
25187 * }
25188 * }
25189 * }
25190 * </pre>
25191 *
25192 * <br/>The `finalizerFn` may be used to manipulate the value before rendering, or to
25193 * apply a custom rendered value. If `aggregation.rendered` is left undefined, the value will be
25194 * rendered. Note that the native aggregation functions use an `finalizerFn` to concatenate
25195 * the label and the value.
25196 *
25197 * <br/>Defaults to {}
25198 */
25199 gridOptions.treeCustomAggregations = gridOptions.treeCustomAggregations || {};
25200 },
25201
25202
25203 /**
25204 * @ngdoc function
25205 * @name treeBaseColumnBuilder
25206 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25207 * @description Sets the tree defaults based on the columnDefs
25208 *
25209 * @param {object} colDef columnDef we're basing on
25210 * @param {GridCol} col the column we're to update
25211 * @param {object} gridOptions the options we should use
25212 * @returns {promise} promise for the builder - actually we do it all inline so it's immediately resolved
25213 */
25214 treeBaseColumnBuilder: function (colDef, col, gridOptions) {
25215
25216
25217 /**
25218 * @ngdoc object
25219 * @name customTreeAggregationFn
25220 * @propertyOf ui.grid.treeBase.api:ColumnDef
25221 * @description A custom function that aggregates rows into some form of
25222 * total. Aggregations run row-by-row, the function needs to be capable of
25223 * creating a running total.
25224 *
25225 * The function will be provided the aggregation item (in which you can store running
25226 * totals), the row value that is to be aggregated, and that same row value converted to
25227 * a number (most aggregations work on numbers)
25228 * @example
25229 * <pre>
25230 * customTreeAggregationFn = function ( aggregation, fieldValue, numValue, row ){
25231 * // calculates the average of the squares of the values
25232 * if ( typeof(aggregation.count) === 'undefined' ){
25233 * aggregation.count = 0;
25234 * }
25235 * aggregation.count++;
25236 *
25237 * if ( !isNaN(numValue) ){
25238 * if ( typeof(aggregation.total) === 'undefined' ){
25239 * aggregation.total = 0;
25240 * }
25241 * aggregation.total = aggregation.total + numValue * numValue;
25242 * }
25243 *
25244 * aggregation.value = aggregation.total / aggregation.count;
25245 * }
25246 * </pre>
25247 * <br/>Defaults to undefined. May be overwritten by treeAggregationType, the two options should not be used together.
25248 */
25249 if ( typeof(colDef.customTreeAggregationFn) !== 'undefined' ){
25250 col.treeAggregationFn = colDef.customTreeAggregationFn;
25251 }
25252
25253 /**
25254 * @ngdoc object
25255 * @name treeAggregationType
25256 * @propertyOf ui.grid.treeBase.api:ColumnDef
25257 * @description Use one of the native or grid-level aggregation methods for calculating aggregations on this column.
25258 * Native method are in the constants file and include: SUM, COUNT, MIN, MAX, AVG. This may also be the property the
25259 * name of an aggregation function defined with {@link ui.grid.treeBase.api:GridOptions treeCustomAggregations}.
25260 *
25261 * <pre>
25262 * treeAggregationType = uiGridTreeBaseConstants.aggregation.SUM,
25263 * }
25264 * </pre>
25265 *
25266 * If you are using aggregations you should either:
25267 *
25268 * - also use grouping, in which case the aggregations are displayed in the group header, OR
25269 * - use treeView, in which case you can set `treeAggregationUpdateEntity: true` in the colDef, and
25270 * treeBase will store the aggregation information in the entity, or you can set `treeAggregationUpdateEntity: false`
25271 * in the colDef, and you need to manual retrieve the calculated aggregations from the row.treeNode.aggregations
25272 *
25273 * <br/>Takes precendence over a treeAggregationFn, the two options should not be used together.
25274 * <br/>Defaults to undefined.
25275 */
25276 if ( typeof(colDef.treeAggregationType) !== 'undefined' ){
25277 col.treeAggregation = { type: colDef.treeAggregationType };
25278 if ( typeof(gridOptions.treeCustomAggregations[colDef.treeAggregationType]) !== 'undefined' ){
25279 col.treeAggregationFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].aggregationFn;
25280 col.treeAggregationFinalizerFn = gridOptions.treeCustomAggregations[colDef.treeAggregationType].finalizerFn;
25281 col.treeAggregation.label = gridOptions.treeCustomAggregations[colDef.treeAggregationType].label;
25282 } else if ( typeof(service.nativeAggregations()[colDef.treeAggregationType]) !== 'undefined' ){
25283 col.treeAggregationFn = service.nativeAggregations()[colDef.treeAggregationType].aggregationFn;
25284 col.treeAggregation.label = service.nativeAggregations()[colDef.treeAggregationType].label;
25285 }
25286 }
25287
25288 /**
25289 * @ngdoc object
25290 * @name treeAggregationLabel
25291 * @propertyOf ui.grid.treeBase.api:ColumnDef
25292 * @description A custom label to use for this aggregation. If provided we don't use native i18n.
25293 */
25294 if ( typeof(colDef.treeAggregationLabel) !== 'undefined' ){
25295 if (typeof(col.treeAggregation) === 'undefined' ){
25296 col.treeAggregation = {};
25297 }
25298 col.treeAggregation.label = colDef.treeAggregationLabel;
25299 }
25300
25301 /**
25302 * @ngdoc object
25303 * @name treeAggregationUpdateEntity
25304 * @propertyOf ui.grid.treeBase.api:ColumnDef
25305 * @description Store calculated aggregations into the entity, allowing them
25306 * to be displayed in the grid using a standard cellTemplate. This defaults to true,
25307 * if you are using grouping then you shouldn't set it to false, as then the aggregations won't
25308 * display.
25309 *
25310 * If you are using treeView in most cases you'll want to set this to true. This will result in
25311 * getCellValue returning the aggregation rather than whatever was stored in the cell attribute on
25312 * the entity. If you want to render the underlying entity value (and do something else with the aggregation)
25313 * then you could use a custom cellTemplate to display `row.entity.myAttribute`, rather than using getCellValue.
25314 *
25315 * <br/>Defaults to true
25316 *
25317 * @example
25318 * <pre>
25319 * gridOptions.columns = [{
25320 * name: 'myCol',
25321 * treeAggregation: { type: uiGridTreeBaseConstants.aggregation.SUM },
25322 * treeAggregationUpdateEntity: true
25323 * cellTemplate: '<div>{{row.entity.myCol + " " + row.treeNode.aggregations[0].rendered}}</div>'
25324 * }];
25325 * </pre>
25326 */
25327 col.treeAggregationUpdateEntity = colDef.treeAggregationUpdateEntity !== false;
25328
25329 /**
25330 * @ngdoc object
25331 * @name customTreeAggregationFinalizerFn
25332 * @propertyOf ui.grid.treeBase.api:ColumnDef
25333 * @description A custom function that populates aggregation.rendered, this is called when
25334 * a particular aggregation has been fully calculated, and we want to render the value.
25335 *
25336 * With the native aggregation options we just concatenate `aggregation.label` and
25337 * `aggregation.value`, but if you wanted to apply a filter or otherwise manipulate the label
25338 * or the value, you can do so with this function. This function will be called after the
25339 * the default `finalizerFn`.
25340 *
25341 * @example
25342 * <pre>
25343 * customTreeAggregationFinalizerFn = function ( aggregation ){
25344 * aggregation.rendered = aggregation.label + aggregation.value / 100 + '%';
25345 * }
25346 * </pre>
25347 * <br/>Defaults to undefined.
25348 */
25349 if ( typeof(col.customTreeAggregationFinalizerFn) === 'undefined' ){
25350 col.customTreeAggregationFinalizerFn = colDef.customTreeAggregationFinalizerFn;
25351 }
25352
25353 },
25354
25355
25356 /**
25357 * @ngdoc function
25358 * @name createRowHeader
25359 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25360 * @description Create the rowHeader. If treeRowHeaderAlwaysVisible then
25361 * set it to visible, otherwise set it to invisible
25362 *
25363 * @param {Grid} grid grid object
25364 */
25365 createRowHeader: function( grid ){
25366 var rowHeaderColumnDef = {
25367 name: uiGridTreeBaseConstants.rowHeaderColName,
25368 displayName: '',
25369 width: grid.options.treeRowHeaderBaseWidth,
25370 minWidth: 10,
25371 cellTemplate: 'ui-grid/treeBaseRowHeader',
25372 headerCellTemplate: 'ui-grid/treeBaseHeaderCell',
25373 enableColumnResizing: false,
25374 enableColumnMenu: false,
25375 exporterSuppressExport: true,
25376 allowCellFocus: true
25377 };
25378
25379 rowHeaderColumnDef.visible = grid.options.treeRowHeaderAlwaysVisible;
25380 grid.addRowHeaderColumn( rowHeaderColumnDef );
25381 },
25382
25383
25384 /**
25385 * @ngdoc function
25386 * @name expandAllRows
25387 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25388 * @description Expands all nodes in the tree
25389 *
25390 * @param {Grid} grid grid object
25391 */
25392 expandAllRows: function (grid) {
25393 grid.treeBase.tree.forEach( function( node ) {
25394 service.setAllNodes( grid, node, uiGridTreeBaseConstants.EXPANDED);
25395 });
25396 grid.treeBase.expandAll = true;
25397 grid.queueGridRefresh();
25398 },
25399
25400
25401 /**
25402 * @ngdoc function
25403 * @name collapseAllRows
25404 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25405 * @description Collapses all nodes in the tree
25406 *
25407 * @param {Grid} grid grid object
25408 */
25409 collapseAllRows: function (grid) {
25410 grid.treeBase.tree.forEach( function( node ) {
25411 service.setAllNodes( grid, node, uiGridTreeBaseConstants.COLLAPSED);
25412 });
25413 grid.treeBase.expandAll = false;
25414 grid.queueGridRefresh();
25415 },
25416
25417
25418 /**
25419 * @ngdoc function
25420 * @name setAllNodes
25421 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25422 * @description Works through a subset of grid.treeBase.rowExpandedStates, setting
25423 * all child nodes (and their descendents) of the provided node to the given state.
25424 *
25425 * Calls itself recursively on all nodes so as to achieve this.
25426 *
25427 * @param {Grid} grid the grid we're operating on (so we can raise events)
25428 * @param {object} treeNode a node in the tree that we want to update
25429 * @param {string} targetState the state we want to set it to
25430 */
25431 setAllNodes: function (grid, treeNode, targetState) {
25432 if ( typeof(treeNode.state) !== 'undefined' && treeNode.state !== targetState ){
25433 treeNode.state = targetState;
25434
25435 if ( targetState === uiGridTreeBaseConstants.EXPANDED ){
25436 grid.api.treeBase.raise.rowExpanded(treeNode.row);
25437 } else {
25438 grid.api.treeBase.raise.rowCollapsed(treeNode.row);
25439 }
25440 }
25441
25442 // set all child nodes
25443 if ( treeNode.children ){
25444 treeNode.children.forEach(function( childNode ){
25445 service.setAllNodes(grid, childNode, targetState);
25446 });
25447 }
25448 },
25449
25450
25451 /**
25452 * @ngdoc function
25453 * @name toggleRowTreeState
25454 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25455 * @description Toggles the expand or collapse state of this grouped row, if
25456 * it's a parent row
25457 *
25458 * @param {Grid} grid grid object
25459 * @param {GridRow} row the row we want to toggle
25460 */
25461 toggleRowTreeState: function ( grid, row ){
25462 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25463 return;
25464 }
25465
25466 if (row.treeNode.state === uiGridTreeBaseConstants.EXPANDED){
25467 service.collapseRow(grid, row);
25468 } else {
25469 service.expandRow(grid, row);
25470 }
25471
25472 grid.queueGridRefresh();
25473 },
25474
25475
25476 /**
25477 * @ngdoc function
25478 * @name expandRow
25479 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25480 * @description Expands this specific row, showing only immediate children.
25481 *
25482 * @param {Grid} grid grid object
25483 * @param {GridRow} row the row we want to expand
25484 */
25485 expandRow: function ( grid, row ){
25486 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25487 return;
25488 }
25489
25490 if ( row.treeNode.state !== uiGridTreeBaseConstants.EXPANDED ){
25491 row.treeNode.state = uiGridTreeBaseConstants.EXPANDED;
25492 grid.api.treeBase.raise.rowExpanded(row);
25493 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
25494 grid.queueGridRefresh();
25495 }
25496 },
25497
25498
25499 /**
25500 * @ngdoc function
25501 * @name expandRowChildren
25502 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25503 * @description Expands this specific row, showing all children.
25504 *
25505 * @param {Grid} grid grid object
25506 * @param {GridRow} row the row we want to expand
25507 */
25508 expandRowChildren: function ( grid, row ){
25509 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25510 return;
25511 }
25512
25513 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.EXPANDED);
25514 grid.treeBase.expandAll = service.allExpanded(grid.treeBase.tree);
25515 grid.queueGridRefresh();
25516 },
25517
25518
25519 /**
25520 * @ngdoc function
25521 * @name collapseRow
25522 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25523 * @description Collapses this specific row
25524 *
25525 * @param {Grid} grid grid object
25526 * @param {GridRow} row the row we want to collapse
25527 */
25528 collapseRow: function( grid, row ){
25529 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25530 return;
25531 }
25532
25533 if ( row.treeNode.state !== uiGridTreeBaseConstants.COLLAPSED ){
25534 row.treeNode.state = uiGridTreeBaseConstants.COLLAPSED;
25535 grid.treeBase.expandAll = false;
25536 grid.api.treeBase.raise.rowCollapsed(row);
25537 grid.queueGridRefresh();
25538 }
25539 },
25540
25541
25542 /**
25543 * @ngdoc function
25544 * @name collapseRowChildren
25545 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25546 * @description Collapses this specific row and all children
25547 *
25548 * @param {Grid} grid grid object
25549 * @param {GridRow} row the row we want to collapse
25550 */
25551 collapseRowChildren: function( grid, row ){
25552 if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){
25553 return;
25554 }
25555
25556 service.setAllNodes(grid, row.treeNode, uiGridTreeBaseConstants.COLLAPSED);
25557 grid.treeBase.expandAll = false;
25558 grid.queueGridRefresh();
25559 },
25560
25561
25562 /**
25563 * @ngdoc function
25564 * @name allExpanded
25565 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25566 * @description Returns true if all rows are expanded, false
25567 * if they're not. Walks the tree to determine this. Used
25568 * to set the expandAll state.
25569 *
25570 * If the node has no children, then return true (it's immaterial
25571 * whether it is expanded). If the node has children, then return
25572 * false if this node is collapsed, or if any child node is not all expanded
25573 *
25574 * @param {object} tree the grid to check
25575 * @returns {boolean} whether or not the tree is all expanded
25576 */
25577 allExpanded: function( tree ){
25578 var allExpanded = true;
25579 tree.forEach( function( node ){
25580 if ( !service.allExpandedInternal( node ) ){
25581 allExpanded = false;
25582 }
25583 });
25584 return allExpanded;
25585 },
25586
25587 allExpandedInternal: function( treeNode ){
25588 if ( treeNode.children && treeNode.children.length > 0 ){
25589 if ( treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
25590 return false;
25591 }
25592 var allExpanded = true;
25593 treeNode.children.forEach( function( node ){
25594 if ( !service.allExpandedInternal( node ) ){
25595 allExpanded = false;
25596 }
25597 });
25598 return allExpanded;
25599 } else {
25600 return true;
25601 }
25602 },
25603
25604
25605 /**
25606 * @ngdoc function
25607 * @name treeRows
25608 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25609 * @description The rowProcessor that adds the nodes to the tree, and sets the visible
25610 * state of each row based on it's parent state
25611 *
25612 * Assumes it is always called after the sorting processor, and the grouping processor if there is one.
25613 * Performs any tree sorts itself after having built the tree
25614 *
25615 * Processes all the rows in order, setting the group level based on the $$treeLevel in the associated
25616 * entity, and setting the visible state based on the parent's state.
25617 *
25618 * Calculates the deepest level of tree whilst it goes, and updates that so that the header column can be correctly
25619 * sized.
25620 *
25621 * Aggregates if necessary along the way.
25622 *
25623 * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor
25624 * @returns {array} the updated rows
25625 */
25626 treeRows: function( renderableRows ) {
25627 if (renderableRows.length === 0){
25628 return renderableRows;
25629 }
25630
25631 var grid = this;
25632 var currentLevel = 0;
25633 var currentState = uiGridTreeBaseConstants.EXPANDED;
25634 var parents = [];
25635
25636 grid.treeBase.tree = service.createTree( grid, renderableRows );
25637 service.updateRowHeaderWidth( grid );
25638
25639 service.sortTree( grid );
25640 service.fixFilter( grid );
25641
25642 return service.renderTree( grid.treeBase.tree );
25643 },
25644
25645
25646 /**
25647 * @ngdoc function
25648 * @name createOrUpdateRowHeaderWidth
25649 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25650 * @description Calculates the rowHeader width.
25651 *
25652 * If rowHeader is always present, updates the width.
25653 *
25654 * If rowHeader is only sometimes present (`treeRowHeaderAlwaysVisible: false`), determines whether there
25655 * should be one, then creates or removes it as appropriate, with the created rowHeader having the
25656 * right width.
25657 *
25658 * If there's never a rowHeader then never creates one: `showTreeRowHeader: false`
25659 *
25660 * @param {Grid} grid the grid we want to set the row header on
25661 */
25662 updateRowHeaderWidth: function( grid ){
25663 var rowHeader = grid.getColumn(uiGridTreeBaseConstants.rowHeaderColName);
25664
25665 var newWidth = grid.options.treeRowHeaderBaseWidth + grid.options.treeIndent * Math.max(grid.treeBase.numberLevels - 1, 0);
25666 if ( rowHeader && newWidth !== rowHeader.width ){
25667 rowHeader.width = newWidth;
25668 grid.queueRefresh();
25669 }
25670
25671 var newVisibility = true;
25672 if ( grid.options.showTreeRowHeader === false ){
25673 newVisibility = false;
25674 }
25675 if ( grid.options.treeRowHeaderAlwaysVisible === false && grid.treeBase.numberLevels <= 0 ){
25676 newVisibility = false;
25677 }
25678 if ( rowHeader.visible !== newVisibility ) {
25679 rowHeader.visible = newVisibility;
25680 rowHeader.colDef.visible = newVisibility;
25681 grid.queueGridRefresh();
25682 }
25683 },
25684
25685
25686 /**
25687 * @ngdoc function
25688 * @name renderTree
25689 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25690 * @description Creates an array of rows based on the tree, exporting only
25691 * the visible nodes and leaves
25692 *
25693 * @param {array} nodeList the list of nodes - can be grid.treeBase.tree, or can be node.children when
25694 * we're calling recursively
25695 * @returns {array} renderable rows
25696 */
25697 renderTree: function( nodeList ){
25698 var renderableRows = [];
25699
25700 nodeList.forEach( function ( node ){
25701 if ( node.row.visible ){
25702 renderableRows.push( node.row );
25703 }
25704 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
25705 renderableRows = renderableRows.concat( service.renderTree( node.children ) );
25706 }
25707 });
25708 return renderableRows;
25709 },
25710
25711
25712 /**
25713 * @ngdoc function
25714 * @name createTree
25715 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25716 * @description Creates a tree from the renderableRows
25717 *
25718 * @param {Grid} grid the grid
25719 * @param {array} renderableRows the rows we want to create a tree from
25720 * @returns {object} the tree we've build
25721 */
25722 createTree: function( grid, renderableRows ) {
25723 var currentLevel = -1;
25724 var parents = [];
25725 var currentState;
25726 grid.treeBase.tree = [];
25727 grid.treeBase.numberLevels = 0;
25728 var aggregations = service.getAggregations( grid );
25729
25730 var createNode = function( row ){
25731 if ( typeof(row.entity.$$treeLevel) !== 'undefined' && row.treeLevel !== row.entity.$$treeLevel ){
25732 row.treeLevel = row.entity.$$treeLevel;
25733 }
25734
25735 if ( row.treeLevel <= currentLevel ){
25736 // pop any levels that aren't parents of this level, formatting the aggregation at the same time
25737 while ( row.treeLevel <= currentLevel ){
25738 var lastParent = parents.pop();
25739 service.finaliseAggregations( lastParent );
25740 currentLevel--;
25741 }
25742
25743 // reset our current state based on the new parent, set to expanded if this is a level 0 node
25744 if ( parents.length > 0 ){
25745 currentState = service.setCurrentState(parents);
25746 } else {
25747 currentState = uiGridTreeBaseConstants.EXPANDED;
25748 }
25749 }
25750
25751 // aggregate if this is a leaf node
25752 if ( ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ) && row.visible ){
25753 service.aggregate( grid, row, parents );
25754 }
25755
25756 // add this node to the tree
25757 service.addOrUseNode(grid, row, parents, aggregations);
25758
25759 if ( typeof(row.treeLevel) !== 'undefined' && row.treeLevel !== null && row.treeLevel >= 0 ){
25760 parents.push(row);
25761 currentLevel++;
25762 currentState = service.setCurrentState(parents);
25763 }
25764
25765 // update the tree number of levels, so we can set header width if we need to
25766 if ( grid.treeBase.numberLevels < row.treeLevel + 1){
25767 grid.treeBase.numberLevels = row.treeLevel + 1;
25768 }
25769 };
25770
25771 renderableRows.forEach( createNode );
25772
25773 // finalise remaining aggregations
25774 while ( parents.length > 0 ){
25775 var lastParent = parents.pop();
25776 service.finaliseAggregations( lastParent );
25777 }
25778
25779 return grid.treeBase.tree;
25780 },
25781
25782
25783 /**
25784 * @ngdoc function
25785 * @name addOrUseNode
25786 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25787 * @description Creates a tree node for this row. If this row already has a treeNode
25788 * recorded against it, preserves the state, but otherwise overwrites the data.
25789 *
25790 * @param {grid} grid the grid we're operating on
25791 * @param {gridRow} row the row we want to set
25792 * @param {array} parents an array of the parents this row should have
25793 * @param {array} aggregationBase empty aggregation information
25794 * @returns {undefined} updates the parents array, updates the row to have a treeNode, and updates the
25795 * grid.treeBase.tree
25796 */
25797 addOrUseNode: function( grid, row, parents, aggregationBase ){
25798 var newAggregations = [];
25799 aggregationBase.forEach( function(aggregation){
25800 newAggregations.push(service.buildAggregationObject(aggregation.col));
25801 });
25802
25803 var newNode = { state: uiGridTreeBaseConstants.COLLAPSED, row: row, parentRow: null, aggregations: newAggregations, children: [] };
25804 if ( row.treeNode ){
25805 newNode.state = row.treeNode.state;
25806 }
25807 if ( parents.length > 0 ){
25808 newNode.parentRow = parents[parents.length - 1];
25809 }
25810 row.treeNode = newNode;
25811
25812 if ( parents.length === 0 ){
25813 grid.treeBase.tree.push( newNode );
25814 } else {
25815 parents[parents.length - 1].treeNode.children.push( newNode );
25816 }
25817 },
25818
25819
25820 /**
25821 * @ngdoc function
25822 * @name setCurrentState
25823 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25824 * @description Looks at the parents array to determine our current state.
25825 * If any node in the hierarchy is collapsed, then return collapsed, otherwise return
25826 * expanded.
25827 *
25828 * @param {array} parents an array of the parents this row should have
25829 * @returns {string} the state we should be setting to any nodes we see
25830 */
25831 setCurrentState: function( parents ){
25832 var currentState = uiGridTreeBaseConstants.EXPANDED;
25833 parents.forEach( function(parent){
25834 if ( parent.treeNode.state === uiGridTreeBaseConstants.COLLAPSED ){
25835 currentState = uiGridTreeBaseConstants.COLLAPSED;
25836 }
25837 });
25838 return currentState;
25839 },
25840
25841
25842 /**
25843 * @ngdoc function
25844 * @name sortTree
25845 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25846 * @description Performs a recursive sort on the tree nodes, sorting the
25847 * children of each node and putting them back into the children array.
25848 *
25849 * Before doing this it turns back on all the sortIgnore - things that were previously
25850 * ignored we process now. Since we're sorting within the nodes, presumably anything
25851 * that was already sorted is how we derived the nodes, we can keep those sorts too.
25852 *
25853 * We only sort tree nodes that are expanded - no point in wasting effort sorting collapsed
25854 * nodes
25855 *
25856 * @param {Grid} grid the grid to get the aggregation information from
25857 * @returns {array} the aggregation information
25858 */
25859 sortTree: function( grid ){
25860 grid.columns.forEach( function( column ) {
25861 if ( column.sort && column.sort.ignoreSort ){
25862 delete column.sort.ignoreSort;
25863 }
25864 });
25865
25866 grid.treeBase.tree = service.sortInternal( grid, grid.treeBase.tree );
25867 },
25868
25869 sortInternal: function( grid, treeList ){
25870 var rows = treeList.map( function( node ){
25871 return node.row;
25872 });
25873
25874 rows = rowSorter.sort( grid, rows, grid.columns );
25875
25876 var treeNodes = rows.map( function( row ){
25877 return row.treeNode;
25878 });
25879
25880 treeNodes.forEach( function( node ){
25881 if ( node.state === uiGridTreeBaseConstants.EXPANDED && node.children && node.children.length > 0 ){
25882 node.children = service.sortInternal( grid, node.children );
25883 }
25884 });
25885
25886 return treeNodes;
25887 },
25888
25889 /**
25890 * @ngdoc function
25891 * @name fixFilter
25892 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25893 * @description After filtering has run, we need to go back through the tree
25894 * and make sure the parent rows are always visible if any of the child rows
25895 * are visible (filtering may make a child visible, but the parent may not
25896 * match the filter criteria)
25897 *
25898 * This has a risk of being computationally expensive, we do it by walking
25899 * the tree and remembering whether there are any invisible nodes on the
25900 * way down.
25901 *
25902 * @param {Grid} grid the grid to fix filters on
25903 */
25904 fixFilter: function( grid ){
25905 var parentsVisible;
25906
25907 grid.treeBase.tree.forEach( function( node ){
25908 if ( node.children && node.children.length > 0 ){
25909 parentsVisible = node.row.visible;
25910 service.fixFilterInternal( node.children, parentsVisible );
25911 }
25912 });
25913 },
25914
25915 fixFilterInternal: function( nodes, parentsVisible) {
25916 nodes.forEach( function( node ){
25917 if ( node.row.visible && !parentsVisible ){
25918 service.setParentsVisible( node );
25919 parentsVisible = true;
25920 }
25921
25922 if ( node.children && node.children.length > 0 ){
25923 if ( service.fixFilterInternal( node.children, ( parentsVisible && node.row.visible ) ) ) {
25924 parentsVisible = true;
25925 }
25926 }
25927 });
25928
25929 return parentsVisible;
25930 },
25931
25932 setParentsVisible: function( node ){
25933 while ( node.parentRow ){
25934 node.parentRow.visible = true;
25935 node = node.parentRow.treeNode;
25936 }
25937 },
25938
25939 /**
25940 * @ngdoc function
25941 * @name buildAggregationObject
25942 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25943 * @description Build the object which is stored on the column for holding meta-data about the aggregation.
25944 * This method should only be called with columns which have an aggregation.
25945 *
25946 * @param {Column} the column which this object relates to
25947 * @returns {object} {col: Column object, label: string, type: string (optional)}
25948 */
25949 buildAggregationObject: function( column ){
25950 var newAggregation = { col: column };
25951
25952 if ( column.treeAggregation && column.treeAggregation.type ){
25953 newAggregation.type = column.treeAggregation.type;
25954 }
25955
25956 if ( column.treeAggregation && column.treeAggregation.label ){
25957 newAggregation.label = column.treeAggregation.label;
25958 }
25959
25960 return newAggregation;
25961 },
25962
25963 /**
25964 * @ngdoc function
25965 * @name getAggregations
25966 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25967 * @description Looks through the grid columns to find those with aggregations,
25968 * and collates the aggregation information into an array, returns that array
25969 *
25970 * @param {Grid} grid the grid to get the aggregation information from
25971 * @returns {array} the aggregation information
25972 */
25973 getAggregations: function( grid ){
25974 var aggregateArray = [];
25975
25976 grid.columns.forEach( function(column){
25977 if ( typeof(column.treeAggregationFn) !== 'undefined' ){
25978 aggregateArray.push( service.buildAggregationObject(column) );
25979
25980 if ( grid.options.showColumnFooter && typeof(column.colDef.aggregationType) === 'undefined' && column.treeAggregation ){
25981 // Add aggregation object for footer
25982 column.treeFooterAggregation = service.buildAggregationObject(column);
25983 column.aggregationType = service.treeFooterAggregationType;
25984 }
25985 }
25986 });
25987 return aggregateArray;
25988 },
25989
25990
25991 /**
25992 * @ngdoc function
25993 * @name aggregate
25994 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
25995 * @description Accumulate the data from this row onto the aggregations for each parent
25996 *
25997 * Iterate over the parents, then iterate over the aggregations for each of those parents,
25998 * and perform the aggregation for each individual aggregation
25999 *
26000 * @param {Grid} grid grid object
26001 * @param {GridRow} row the row we want to set grouping visibility on
26002 * @param {array} parents the parents that we would want to aggregate onto
26003 */
26004 aggregate: function( grid, row, parents ){
26005 if ( parents.length === 0 && row.treeNode && row.treeNode.aggregations ){
26006 row.treeNode.aggregations.forEach(function(aggregation){
26007 // Calculate aggregations for footer even if there are no grouped rows
26008 if ( typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ) {
26009 var fieldValue = grid.getCellValue(row, aggregation.col);
26010 var numValue = Number(fieldValue);
26011 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26012 }
26013 });
26014 }
26015
26016 parents.forEach( function( parent, index ){
26017 if ( parent.treeNode.aggregations ){
26018 parent.treeNode.aggregations.forEach( function( aggregation ){
26019 var fieldValue = grid.getCellValue(row, aggregation.col);
26020 var numValue = Number(fieldValue);
26021 aggregation.col.treeAggregationFn(aggregation, fieldValue, numValue, row);
26022
26023 if ( index === 0 && typeof(aggregation.col.treeFooterAggregation) !== 'undefined' ){
26024 aggregation.col.treeAggregationFn(aggregation.col.treeFooterAggregation, fieldValue, numValue, row);
26025 }
26026 });
26027 }
26028 });
26029 },
26030
26031
26032 // Aggregation routines - no doco needed as self evident
26033 nativeAggregations: function() {
26034 var nativeAggregations = {
26035 count: {
26036 label: i18nService.get().aggregation.count,
26037 menuTitle: i18nService.get().grouping.aggregate_count,
26038 aggregationFn: function (aggregation, fieldValue, numValue) {
26039 if (typeof(aggregation.value) === 'undefined') {
26040 aggregation.value = 1;
26041 } else {
26042 aggregation.value++;
26043 }
26044 }
26045 },
26046
26047 sum: {
26048 label: i18nService.get().aggregation.sum,
26049 menuTitle: i18nService.get().grouping.aggregate_sum,
26050 aggregationFn: function( aggregation, fieldValue, numValue ) {
26051 if (!isNaN(numValue)) {
26052 if (typeof(aggregation.value) === 'undefined') {
26053 aggregation.value = numValue;
26054 } else {
26055 aggregation.value += numValue;
26056 }
26057 }
26058 }
26059 },
26060
26061 min: {
26062 label: i18nService.get().aggregation.min,
26063 menuTitle: i18nService.get().grouping.aggregate_min,
26064 aggregationFn: function( aggregation, fieldValue, numValue ) {
26065 if (typeof(aggregation.value) === 'undefined') {
26066 aggregation.value = fieldValue;
26067 } else {
26068 if (typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)) {
26069 aggregation.value = fieldValue;
26070 }
26071 }
26072 }
26073 },
26074
26075 max: {
26076 label: i18nService.get().aggregation.max,
26077 menuTitle: i18nService.get().grouping.aggregate_max,
26078 aggregationFn: function( aggregation, fieldValue, numValue ){
26079 if ( typeof(aggregation.value) === 'undefined' ){
26080 aggregation.value = fieldValue;
26081 } else {
26082 if ( typeof(fieldValue) !== 'undefined' && fieldValue !== null && (fieldValue > aggregation.value || aggregation.value === null)){
26083 aggregation.value = fieldValue;
26084 }
26085 }
26086 }
26087 },
26088
26089 avg: {
26090 label: i18nService.get().aggregation.avg,
26091 menuTitle: i18nService.get().grouping.aggregate_avg,
26092 aggregationFn: function( aggregation, fieldValue, numValue ){
26093 if ( typeof(aggregation.count) === 'undefined' ){
26094 aggregation.count = 1;
26095 } else {
26096 aggregation.count++;
26097 }
26098
26099 if ( isNaN(numValue) ){
26100 return;
26101 }
26102
26103 if ( typeof(aggregation.value) === 'undefined' || typeof(aggregation.sum) === 'undefined' ){
26104 aggregation.value = numValue;
26105 aggregation.sum = numValue;
26106 } else {
26107 aggregation.sum += numValue;
26108 aggregation.value = aggregation.sum / aggregation.count;
26109 }
26110 }
26111 }
26112 };
26113 return nativeAggregations;
26114 },
26115
26116 /**
26117 * @ngdoc function
26118 * @name finaliseAggregation
26119 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26120 * @description Helper function used to finalize aggregation nodes and footer cells
26121 *
26122 * @param {gridRow} row the parent we're finalising
26123 * @param {aggregation} the aggregation object manipulated by the aggregationFn
26124 */
26125 finaliseAggregation: function(row, aggregation){
26126 if ( aggregation.col.treeAggregationUpdateEntity && typeof(row) !== 'undefined' && typeof(row.entity[ '$$' + aggregation.col.uid ]) !== 'undefined' ){
26127 angular.extend( aggregation, row.entity[ '$$' + aggregation.col.uid ]);
26128 }
26129
26130 if ( typeof(aggregation.col.treeAggregationFinalizerFn) === 'function' ){
26131 aggregation.col.treeAggregationFinalizerFn( aggregation );
26132 }
26133 if ( typeof(aggregation.col.customTreeAggregationFinalizerFn) === 'function' ){
26134 aggregation.col.customTreeAggregationFinalizerFn( aggregation );
26135 }
26136 if ( typeof(aggregation.rendered) === 'undefined' ){
26137 aggregation.rendered = aggregation.label ? aggregation.label + aggregation.value : aggregation.value;
26138 }
26139 },
26140
26141 /**
26142 * @ngdoc function
26143 * @name finaliseAggregations
26144 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26145 * @description Format the data from the aggregation into the rendered text
26146 * e.g. if we had label: 'sum: ' and value: 25, we'd create 'sum: 25'.
26147 *
26148 * As part of this we call any formatting callback routines we've been provided.
26149 *
26150 * We write our aggregation out to the row.entity if treeAggregationUpdateEntity is
26151 * set on the column - we don't overwrite any information that's already there, we append
26152 * to it so that grouping can have set the groupVal beforehand without us overwriting it.
26153 *
26154 * We need to copy the data from the row.entity first before we finalise the aggregation,
26155 * we need that information for the finaliserFn
26156 *
26157 * @param {gridRow} row the parent we're finalising
26158 */
26159 finaliseAggregations: function( row ){
26160 if ( typeof(row.treeNode.aggregations) === 'undefined' ){
26161 return;
26162 }
26163
26164 row.treeNode.aggregations.forEach( function( aggregation ) {
26165 service.finaliseAggregation(row, aggregation);
26166
26167 if ( aggregation.col.treeAggregationUpdateEntity ){
26168 var aggregationCopy = {};
26169 angular.forEach( aggregation, function( value, key ){
26170 if ( aggregation.hasOwnProperty(key) && key !== 'col' ){
26171 aggregationCopy[key] = value;
26172 }
26173 });
26174
26175 row.entity[ '$$' + aggregation.col.uid ] = aggregationCopy;
26176 }
26177 });
26178 },
26179
26180 /**
26181 * @ngdoc function
26182 * @name treeFooterAggregationType
26183 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26184 * @description Uses the tree aggregation functions and finalizers to set the
26185 * column footer aggregations.
26186 *
26187 * @param {rows} visible rows. not used, but accepted to match signature of GridColumn.aggregationType
26188 * @param {gridColumn} the column we are finalizing
26189 */
26190 treeFooterAggregationType: function( rows, column ) {
26191 service.finaliseAggregation(undefined, column.treeFooterAggregation);
26192 if ( typeof(column.treeFooterAggregation.value) === 'undefined' || column.treeFooterAggregation.rendered === null ){
26193 // The was apparently no aggregation performed (perhaps this is a grouped column
26194 return '';
26195 }
26196 return column.treeFooterAggregation.rendered;
26197 }
26198 };
26199
26200 return service;
26201
26202 }]);
26203
26204
26205 /**
26206 * @ngdoc directive
26207 * @name ui.grid.treeBase.directive:uiGridTreeRowHeaderButtons
26208 * @element div
26209 *
26210 * @description Provides the expand/collapse button on rows
26211 */
26212 module.directive('uiGridTreeBaseRowHeaderButtons', ['$templateCache', 'uiGridTreeBaseService',
26213 function ($templateCache, uiGridTreeBaseService) {
26214 return {
26215 replace: true,
26216 restrict: 'E',
26217 template: $templateCache.get('ui-grid/treeBaseRowHeaderButtons'),
26218 scope: true,
26219 require: '^uiGrid',
26220 link: function($scope, $elm, $attrs, uiGridCtrl) {
26221 var self = uiGridCtrl.grid;
26222 $scope.treeButtonClick = function(row, evt) {
26223 uiGridTreeBaseService.toggleRowTreeState(self, row, evt);
26224 };
26225 }
26226 };
26227 }]);
26228
26229
26230 /**
26231 * @ngdoc directive
26232 * @name ui.grid.treeBase.directive:uiGridTreeBaseExpandAllButtons
26233 * @element div
26234 *
26235 * @description Provides the expand/collapse all button
26236 */
26237 module.directive('uiGridTreeBaseExpandAllButtons', ['$templateCache', 'uiGridTreeBaseService',
26238 function ($templateCache, uiGridTreeBaseService) {
26239 return {
26240 replace: true,
26241 restrict: 'E',
26242 template: $templateCache.get('ui-grid/treeBaseExpandAllButtons'),
26243 scope: false,
26244 link: function($scope, $elm, $attrs, uiGridCtrl) {
26245 var self = $scope.col.grid;
26246
26247 $scope.headerButtonClick = function(row, evt) {
26248 if ( self.treeBase.expandAll ){
26249 uiGridTreeBaseService.collapseAllRows(self, evt);
26250 } else {
26251 uiGridTreeBaseService.expandAllRows(self, evt);
26252 }
26253 };
26254 }
26255 };
26256 }]);
26257
26258
26259 /**
26260 * @ngdoc directive
26261 * @name ui.grid.treeBase.directive:uiGridViewport
26262 * @element div
26263 *
26264 * @description Stacks on top of ui.grid.uiGridViewport to set formatting on a tree header row
26265 */
26266 module.directive('uiGridViewport',
26267 ['$compile', 'uiGridConstants', 'gridUtil', '$parse',
26268 function ($compile, uiGridConstants, gridUtil, $parse) {
26269 return {
26270 priority: -200, // run after default directive
26271 scope: false,
26272 compile: function ($elm, $attrs) {
26273 var rowRepeatDiv = angular.element($elm.children().children()[0]);
26274
26275 var existingNgClass = rowRepeatDiv.attr("ng-class");
26276 var newNgClass = '';
26277 if ( existingNgClass ) {
26278 newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-tree-header-row': row.treeLevel > -1}";
26279 } else {
26280 newNgClass = "{'ui-grid-tree-header-row': row.treeLevel > -1}";
26281 }
26282 rowRepeatDiv.attr("ng-class", newNgClass);
26283
26284 return {
26285 pre: function ($scope, $elm, $attrs, controllers) {
26286
26287 },
26288 post: function ($scope, $elm, $attrs, controllers) {
26289 }
26290 };
26291 }
26292 };
26293 }]);
26294 })();
26295
26296 (function () {
26297 'use strict';
26298
26299 /**
26300 * @ngdoc overview
26301 * @name ui.grid.treeView
26302 * @description
26303 *
26304 * # ui.grid.treeView
26305 *
26306 * <div class="alert alert-warning" role="alert"><strong>Beta</strong> This feature is ready for testing, but it either hasn't seen a lot of use or has some known bugs.</div>
26307 *
26308 * This module provides a tree view of the data that it is provided, with nodes in that
26309 * tree and leaves. Unlike grouping, the tree is an inherent property of the data and must
26310 * be provided with your data array.
26311 *
26312 * Design information:
26313 * -------------------
26314 *
26315 * TreeView uses treeBase for the underlying functionality, and is a very thin wrapper around
26316 * that logic. Most of the design information has now moved to treebase.
26317 * <br/>
26318 * <br/>
26319 *
26320 * <div doc-module-components="ui.grid.treeView"></div>
26321 */
26322
26323 var module = angular.module('ui.grid.treeView', ['ui.grid', 'ui.grid.treeBase']);
26324
26325 /**
26326 * @ngdoc object
26327 * @name ui.grid.treeView.constant:uiGridTreeViewConstants
26328 *
26329 * @description constants available in treeView module, this includes
26330 * all the constants declared in the treeBase module (these are manually copied
26331 * as there isn't an easy way to include constants in another constants file, and
26332 * we don't want to make users include treeBase)
26333 *
26334 */
26335 module.constant('uiGridTreeViewConstants', {
26336 featureName: "treeView",
26337 rowHeaderColName: 'treeBaseRowHeaderCol',
26338 EXPANDED: 'expanded',
26339 COLLAPSED: 'collapsed',
26340 aggregation: {
26341 COUNT: 'count',
26342 SUM: 'sum',
26343 MAX: 'max',
26344 MIN: 'min',
26345 AVG: 'avg'
26346 }
26347 });
26348
26349 /**
26350 * @ngdoc service
26351 * @name ui.grid.treeView.service:uiGridTreeViewService
26352 *
26353 * @description Services for treeView features
26354 */
26355 module.service('uiGridTreeViewService', ['$q', 'uiGridTreeViewConstants', 'uiGridTreeBaseConstants', 'uiGridTreeBaseService', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants',
26356 function ($q, uiGridTreeViewConstants, uiGridTreeBaseConstants, uiGridTreeBaseService, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants) {
26357
26358 var service = {
26359
26360 initializeGrid: function (grid, $scope) {
26361 uiGridTreeBaseService.initializeGrid( grid, $scope );
26362
26363 /**
26364 * @ngdoc object
26365 * @name ui.grid.treeView.grid:treeView
26366 *
26367 * @description Grid properties and functions added for treeView
26368 */
26369 grid.treeView = {};
26370
26371 grid.registerRowsProcessor(service.adjustSorting, 60);
26372
26373 /**
26374 * @ngdoc object
26375 * @name ui.grid.treeView.api:PublicApi
26376 *
26377 * @description Public Api for treeView feature
26378 */
26379 var publicApi = {
26380 events: {
26381 treeView: {
26382 }
26383 },
26384 methods: {
26385 treeView: {
26386 }
26387 }
26388 };
26389
26390 grid.api.registerEventsFromObject(publicApi.events);
26391
26392 grid.api.registerMethodsFromObject(publicApi.methods);
26393
26394 },
26395
26396 defaultGridOptions: function (gridOptions) {
26397 //default option to true unless it was explicitly set to false
26398 /**
26399 * @ngdoc object
26400 * @name ui.grid.treeView.api:GridOptions
26401 *
26402 * @description GridOptions for treeView feature, these are available to be
26403 * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions}
26404 *
26405 * Many tree options are set on treeBase, make sure to look at that feature in
26406 * conjunction with these options.
26407 */
26408
26409 /**
26410 * @ngdoc object
26411 * @name enableTreeView
26412 * @propertyOf ui.grid.treeView.api:GridOptions
26413 * @description Enable row tree view for entire grid.
26414 * <br/>Defaults to true
26415 */
26416 gridOptions.enableTreeView = gridOptions.enableTreeView !== false;
26417
26418 },
26419
26420
26421 /**
26422 * @ngdoc function
26423 * @name adjustSorting
26424 * @methodOf ui.grid.treeBase.service:uiGridTreeBaseService
26425 * @description Trees cannot be sorted the same as flat lists of rows -
26426 * trees are sorted recursively within each level - so the children of each
26427 * node are sorted, but not the full set of rows.
26428 *
26429 * To achieve this, we suppress the normal sorting by setting ignoreSort on
26430 * each of the sort columns. When the treeBase rowsProcessor runs it will then
26431 * unignore these, and will perform a recursive sort against the tree that it builds.
26432 *
26433 * @param {array} renderableRows the rows that we need to pass on through
26434 * @returns {array} renderableRows that we passed on through
26435 */
26436 adjustSorting: function( renderableRows ) {
26437 var grid = this;
26438
26439 grid.columns.forEach( function( column ){
26440 if ( column.sort ){
26441 column.sort.ignoreSort = true;
26442 }
26443 });
26444
26445 return renderableRows;
26446 }
26447
26448 };
26449
26450 return service;
26451
26452 }]);
26453
26454 /**
26455 * @ngdoc directive
26456 * @name ui.grid.treeView.directive:uiGridTreeView
26457 * @element div
26458 * @restrict A
26459 *
26460 * @description Adds treeView features to grid
26461 *
26462 * @example
26463 <example module="app">
26464 <file name="app.js">
26465 var app = angular.module('app', ['ui.grid', 'ui.grid.treeView']);
26466
26467 app.controller('MainCtrl', ['$scope', function ($scope) {
26468 $scope.data = [
26469 { name: 'Bob', title: 'CEO' },
26470 { name: 'Frank', title: 'Lowly Developer' }
26471 ];
26472
26473 $scope.columnDefs = [
26474 {name: 'name', enableCellEdit: true},
26475 {name: 'title', enableCellEdit: true}
26476 ];
26477
26478 $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data };
26479 }]);
26480 </file>
26481 <file name="index.html">
26482 <div ng-controller="MainCtrl">
26483 <div ui-grid="gridOptions" ui-grid-tree-view></div>
26484 </div>
26485 </file>
26486 </example>
26487 */
26488 module.directive('uiGridTreeView', ['uiGridTreeViewConstants', 'uiGridTreeViewService', '$templateCache',
26489 function (uiGridTreeViewConstants, uiGridTreeViewService, $templateCache) {
26490 return {
26491 replace: true,
26492 priority: 0,
26493 require: '^uiGrid',
26494 scope: false,
26495 compile: function () {
26496 return {
26497 pre: function ($scope, $elm, $attrs, uiGridCtrl) {
26498 if (uiGridCtrl.grid.options.enableTreeView !== false){
26499 uiGridTreeViewService.initializeGrid(uiGridCtrl.grid, $scope);
26500 }
26501 },
26502 post: function ($scope, $elm, $attrs, uiGridCtrl) {
26503
26504 }
26505 };
26506 }
26507 };
26508 }]);
26509 })();
26510
26511 angular.module('ui.grid').run(['$templateCache', function($templateCache) {
26512 'use strict';
26513
26514 $templateCache.put('ui-grid/ui-grid-filter',
26515 "<div class=\"ui-grid-filter-container\" ng-repeat=\"colFilter in col.filters\" ng-class=\"{'ui-grid-filter-cancel-button-hidden' : colFilter.disableCancelFilterButton === true }\"><div ng-if=\"colFilter.type !== 'select'\"><input type=\"text\" class=\"ui-grid-filter-input ui-grid-filter-input-{{$index}}\" ng-model=\"colFilter.term\" ng-attr-placeholder=\"{{colFilter.placeholder || ''}}\" aria-label=\"{{colFilter.ariaLabel || aria.defaultFilterLabel}}\"><div role=\"button\" class=\"ui-grid-filter-button\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term !== null && colFilter.term !== ''\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\">&nbsp;</i></div></div><div ng-if=\"colFilter.type === 'select'\"><select class=\"ui-grid-filter-select ui-grid-filter-input-{{$index}}\" ng-model=\"colFilter.term\" ng-attr-placeholder=\"{{colFilter.placeholder || aria.defaultFilterLabel}}\" aria-label=\"{{colFilter.ariaLabel || ''}}\" ng-options=\"option.value as option.label for option in colFilter.selectOptions\"><option value=\"\"></option></select><div role=\"button\" class=\"ui-grid-filter-button-select\" ng-click=\"removeFilter(colFilter, $index)\" ng-if=\"!colFilter.disableCancelFilterButton\" ng-disabled=\"colFilter.term === undefined || colFilter.term === null || colFilter.term === ''\" ng-show=\"colFilter.term !== undefined && colFilter.term != null\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"aria.removeFilter\">&nbsp;</i></div></div></div>"
26516 );
26517
26518
26519 $templateCache.put('ui-grid/ui-grid-footer',
26520 "<div class=\"ui-grid-footer-panel ui-grid-footer-aggregates-row\"><!-- tfooter --><div class=\"ui-grid-footer ui-grid-footer-viewport\"><div class=\"ui-grid-footer-canvas\"><div class=\"ui-grid-footer-cell-wrapper\" ng-style=\"colContainer.headerCellWrapperStyle()\"><div role=\"row\" class=\"ui-grid-footer-cell-row\"><div ui-grid-footer-cell role=\"gridcell\" ng-repeat=\"col in colContainer.renderedColumns track by col.uid\" col=\"col\" render-index=\"$index\" class=\"ui-grid-footer-cell ui-grid-clearfix\"></div></div></div></div></div></div>"
26521 );
26522
26523
26524 $templateCache.put('ui-grid/ui-grid-grid-footer',
26525 "<div class=\"ui-grid-footer-info ui-grid-grid-footer\"><span>{{'search.totalItems' | t}} {{grid.rows.length}}</span> <span ng-if=\"grid.renderContainers.body.visibleRowCache.length !== grid.rows.length\" class=\"ngLabel\">({{\"search.showingItems\" | t}} {{grid.renderContainers.body.visibleRowCache.length}})</span></div>"
26526 );
26527
26528
26529 $templateCache.put('ui-grid/ui-grid-group-panel',
26530 "<div class=\"ui-grid-group-panel\"><div ui-t=\"groupPanel.description\" class=\"description\" ng-show=\"groupings.length == 0\"></div><ul ng-show=\"groupings.length > 0\" class=\"ngGroupList\"><li class=\"ngGroupItem\" ng-repeat=\"group in configGroups\"><span class=\"ngGroupElement\"><span class=\"ngGroupName\">{{group.displayName}} <span ng-click=\"removeGroup($index)\" class=\"ngRemoveGroup\">x</span></span> <span ng-hide=\"$last\" class=\"ngGroupArrow\"></span></span></li></ul></div>"
26531 );
26532
26533
26534 $templateCache.put('ui-grid/ui-grid-header',
26535 "<div role=\"rowgroup\" class=\"ui-grid-header\"><!-- theader --><div class=\"ui-grid-top-panel\"><div class=\"ui-grid-header-viewport\"><div class=\"ui-grid-header-canvas\"><div class=\"ui-grid-header-cell-wrapper\" ng-style=\"colContainer.headerCellWrapperStyle()\"><div role=\"row\" class=\"ui-grid-header-cell-row\"><div class=\"ui-grid-header-cell ui-grid-clearfix\" ng-repeat=\"col in colContainer.renderedColumns track by col.uid\" ui-grid-header-cell col=\"col\" render-index=\"$index\"></div></div></div></div></div></div></div>"
26536 );
26537
26538
26539 $templateCache.put('ui-grid/ui-grid-menu-button',
26540 "<div class=\"ui-grid-menu-button\"><div role=\"button\" ui-grid-one-bind-id-grid=\"'grid-menu'\" class=\"ui-grid-icon-container\" ng-click=\"toggleMenu()\" aria-haspopup=\"true\"><i class=\"ui-grid-icon-menu\" ui-grid-one-bind-aria-label=\"i18n.aria.buttonLabel\">&nbsp;</i></div><div ui-grid-menu menu-items=\"menuItems\"></div></div>"
26541 );
26542
26543
26544 $templateCache.put('ui-grid/ui-grid-no-header',
26545 "<div class=\"ui-grid-top-panel\"></div>"
26546 );
26547
26548
26549 $templateCache.put('ui-grid/ui-grid-row',
26550 "<div ng-repeat=\"(colRenderIndex, col) in colContainer.renderedColumns track by col.uid\" ui-grid-one-bind-id-grid=\"rowRenderIndex + '-' + col.uid + '-cell'\" class=\"ui-grid-cell\" ng-class=\"{ 'ui-grid-row-header-cell': col.isRowHeader }\" role=\"{{col.isRowHeader ? 'rowheader' : 'gridcell'}}\" ui-grid-cell></div>"
26551 );
26552
26553
26554 $templateCache.put('ui-grid/ui-grid',
26555 "<div ui-i18n=\"en\" class=\"ui-grid\"><!-- TODO (c0bra): add \"scoped\" attr here, eventually? --><style ui-grid-style>.grid{{ grid.id }} {\n" +
26556 " /* Styles for the grid */\n" +
26557 " }\n" +
26558 "\n" +
26559 " .grid{{ grid.id }} .ui-grid-row, .grid{{ grid.id }} .ui-grid-cell, .grid{{ grid.id }} .ui-grid-cell .ui-grid-vertical-bar {\n" +
26560 " height: {{ grid.options.rowHeight }}px;\n" +
26561 " }\n" +
26562 "\n" +
26563 " .grid{{ grid.id }} .ui-grid-row:last-child .ui-grid-cell {\n" +
26564 " border-bottom-width: {{ ((grid.getTotalRowHeight() < grid.getViewportHeight()) && '1') || '0' }}px;\n" +
26565 " }\n" +
26566 "\n" +
26567 " {{ grid.verticalScrollbarStyles }}\n" +
26568 " {{ grid.horizontalScrollbarStyles }}\n" +
26569 "\n" +
26570 " /*\n" +
26571 " .ui-grid[dir=rtl] .ui-grid-viewport {\n" +
26572 " padding-left: {{ grid.verticalScrollbarWidth }}px;\n" +
26573 " }\n" +
26574 " */\n" +
26575 "\n" +
26576 " {{ grid.customStyles }}</style><div class=\"ui-grid-contents-wrapper\"><div ui-grid-menu-button ng-if=\"grid.options.enableGridMenu\"></div><div ng-if=\"grid.hasLeftContainer()\" style=\"width: 0\" ui-grid-pinned-container=\"'left'\"></div><div ui-grid-render-container container-id=\"'body'\" col-container-name=\"'body'\" row-container-name=\"'body'\" bind-scroll-horizontal=\"true\" bind-scroll-vertical=\"true\" enable-horizontal-scrollbar=\"grid.options.enableHorizontalScrollbar\" enable-vertical-scrollbar=\"grid.options.enableVerticalScrollbar\"></div><div ng-if=\"grid.hasRightContainer()\" style=\"width: 0\" ui-grid-pinned-container=\"'right'\"></div><div ui-grid-grid-footer ng-if=\"grid.options.showGridFooter\"></div><div ui-grid-column-menu ng-if=\"grid.options.enableColumnMenus\"></div><div ng-transclude></div></div></div>"
26577 );
26578
26579
26580 $templateCache.put('ui-grid/uiGridCell',
26581 "<div class=\"ui-grid-cell-contents\" title=\"TOOLTIP\">{{COL_FIELD CUSTOM_FILTERS}}</div>"
26582 );
26583
26584
26585 $templateCache.put('ui-grid/uiGridColumnMenu',
26586 "<div class=\"ui-grid-column-menu\"><div ui-grid-menu menu-items=\"menuItems\"><!-- <div class=\"ui-grid-column-menu\">\n" +
26587 " <div class=\"inner\" ng-show=\"menuShown\">\n" +
26588 " <ul>\n" +
26589 " <div ng-show=\"grid.options.enableSorting\">\n" +
26590 " <li ng-click=\"sortColumn($event, asc)\" ng-class=\"{ 'selected' : col.sort.direction == asc }\"><i class=\"ui-grid-icon-sort-alt-up\"></i> Sort Ascending</li>\n" +
26591 " <li ng-click=\"sortColumn($event, desc)\" ng-class=\"{ 'selected' : col.sort.direction == desc }\"><i class=\"ui-grid-icon-sort-alt-down\"></i> Sort Descending</li>\n" +
26592 " <li ng-show=\"col.sort.direction\" ng-click=\"unsortColumn()\"><i class=\"ui-grid-icon-cancel\"></i> Remove Sort</li>\n" +
26593 " </div>\n" +
26594 " </ul>\n" +
26595 " </div>\n" +
26596 " </div> --></div></div>"
26597 );
26598
26599
26600 $templateCache.put('ui-grid/uiGridFooterCell',
26601 "<div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><div>{{ col.getAggregationText() + ( col.getAggregationValue() CUSTOM_FILTERS ) }}</div></div>"
26602 );
26603
26604
26605 $templateCache.put('ui-grid/uiGridHeaderCell',
26606 "<div role=\"columnheader\" ng-class=\"{ 'sortable': sortable }\" ui-grid-one-bind-aria-labelledby-grid=\"col.uid + '-header-text ' + col.uid + '-sortdir-text'\" aria-sort=\"{{col.sort.direction == asc ? 'ascending' : ( col.sort.direction == desc ? 'descending' : (!col.sort.direction ? 'none' : 'other'))}}\"><div role=\"button\" tabindex=\"0\" class=\"ui-grid-cell-contents ui-grid-header-cell-primary-focus\" col-index=\"renderIndex\" title=\"TOOLTIP\"><span class=\"ui-grid-header-cell-label\" ui-grid-one-bind-id-grid=\"col.uid + '-header-text'\">{{ col.displayName CUSTOM_FILTERS }}</span> <span ui-grid-one-bind-id-grid=\"col.uid + '-sortdir-text'\" ui-grid-visible=\"col.sort.direction\" aria-label=\"{{getSortDirectionAriaLabel()}}\"><i ng-class=\"{ 'ui-grid-icon-up-dir': col.sort.direction == asc, 'ui-grid-icon-down-dir': col.sort.direction == desc, 'ui-grid-icon-blank': !col.sort.direction }\" title=\"{{col.sort.priority ? i18n.headerCell.priority + ' ' + col.sort.priority : null}}\" aria-hidden=\"true\"></i> <sub class=\"ui-grid-sort-priority-number\">{{col.sort.priority}}</sub></span></div><div role=\"button\" tabindex=\"0\" ui-grid-one-bind-id-grid=\"col.uid + '-menu-button'\" class=\"ui-grid-column-menu-button\" ng-if=\"grid.options.enableColumnMenus && !col.isRowHeader && col.colDef.enableColumnMenu !== false\" ng-click=\"toggleMenu($event)\" ng-class=\"{'ui-grid-column-menu-button-last-col': isLastCol}\" ui-grid-one-bind-aria-label=\"i18n.headerCell.aria.columnMenuButtonLabel\" aria-haspopup=\"true\"><i class=\"ui-grid-icon-angle-down\" aria-hidden=\"true\">&nbsp;</i></div><div ui-grid-filter></div></div>"
26607 );
26608
26609
26610 $templateCache.put('ui-grid/uiGridMenu',
26611 "<div class=\"ui-grid-menu\" ng-if=\"shown\"><div class=\"ui-grid-menu-mid\" ng-show=\"shownMid\"><div class=\"ui-grid-menu-inner\"><button type=\"button\" ng-focus=\"focus=true\" ng-blur=\"focus=false\" class=\"ui-grid-menu-close-button\" ng-class=\"{'ui-grid-sr-only': (!focus)}\"><i class=\"ui-grid-icon-cancel\" ui-grid-one-bind-aria-label=\"i18n.close\"></i></button><ul role=\"menu\" class=\"ui-grid-menu-items\"><li ng-repeat=\"item in menuItems\" role=\"menuitem\" ui-grid-menu-item ui-grid-one-bind-id=\"'menuitem-'+$index\" action=\"item.action\" name=\"item.title\" active=\"item.active\" icon=\"item.icon\" shown=\"item.shown\" context=\"item.context\" template-url=\"item.templateUrl\" leave-open=\"item.leaveOpen\" screen-reader-only=\"item.screenReaderOnly\"></li></ul></div></div></div>"
26612 );
26613
26614
26615 $templateCache.put('ui-grid/uiGridMenuItem',
26616 "<button type=\"button\" class=\"ui-grid-menu-item\" ng-click=\"itemAction($event, title)\" ng-show=\"itemShown()\" ng-class=\"{ 'ui-grid-menu-item-active': active(), 'ui-grid-sr-only': (!focus && screenReaderOnly) }\" aria-pressed=\"{{active()}}\" tabindex=\"0\" ng-focus=\"focus=true\" ng-blur=\"focus=false\"><i ng-class=\"icon\" aria-hidden=\"true\">&nbsp;</i> {{ name }}</button>"
26617 );
26618
26619
26620 $templateCache.put('ui-grid/uiGridRenderContainer',
26621 "<div role=\"grid\" ui-grid-one-bind-id-grid=\"'grid-container'\" class=\"ui-grid-render-container\" ng-style=\"{ 'margin-left': colContainer.getMargin('left') + 'px', 'margin-right': colContainer.getMargin('right') + 'px' }\"><!-- All of these dom elements are replaced in place --><div ui-grid-header></div><div ui-grid-viewport></div><div ng-if=\"colContainer.needsHScrollbarPlaceholder()\" class=\"ui-grid-scrollbar-placeholder\" ng-style=\"{height:colContainer.grid.scrollbarHeight + 'px'}\"></div><ui-grid-footer ng-if=\"grid.options.showColumnFooter\"></ui-grid-footer></div>"
26622 );
26623
26624
26625 $templateCache.put('ui-grid/uiGridViewport',
26626 "<div role=\"rowgroup\" class=\"ui-grid-viewport\" ng-style=\"colContainer.getViewportStyle()\"><!-- tbody --><div class=\"ui-grid-canvas\"><div ng-repeat=\"(rowRenderIndex, row) in rowContainer.renderedRows track by $index\" class=\"ui-grid-row\" ng-style=\"Viewport.rowStyle(rowRenderIndex)\"><div role=\"row\" ui-grid-row=\"row\" row-render-index=\"rowRenderIndex\"></div></div></div></div>"
26627 );
26628
26629
26630 $templateCache.put('ui-grid/cellEditor',
26631 "<div><form name=\"inputForm\"><input type=\"INPUT_TYPE\" ng-class=\"'colt' + col.uid\" ui-grid-editor ng-model=\"MODEL_COL_FIELD\"></form></div>"
26632 );
26633
26634
26635 $templateCache.put('ui-grid/dropdownEditor',
26636 "<div><form name=\"inputForm\"><select ng-class=\"'colt' + col.uid\" ui-grid-edit-dropdown ng-model=\"MODEL_COL_FIELD\" ng-options=\"field[editDropdownIdLabel] as field[editDropdownValueLabel] CUSTOM_FILTERS for field in editDropdownOptionsArray\"></select></form></div>"
26637 );
26638
26639
26640 $templateCache.put('ui-grid/fileChooserEditor',
26641 "<div><form name=\"inputForm\"><input ng-class=\"'colt' + col.uid\" ui-grid-edit-file-chooser type=\"file\" id=\"files\" name=\"files[]\" ng-model=\"MODEL_COL_FIELD\"></form></div>"
26642 );
26643
26644
26645 $templateCache.put('ui-grid/expandableRow',
26646 "<div ui-grid-expandable-row ng-if=\"expandableRow.shouldRenderExpand()\" class=\"expandableRow\" style=\"float:left; margin-top: 1px; margin-bottom: 1px\" ng-style=\"{width: (grid.renderContainers.body.getCanvasWidth()) + 'px', height: row.expandedRowHeight + 'px'}\"></div>"
26647 );
26648
26649
26650 $templateCache.put('ui-grid/expandableRowHeader',
26651 "<div class=\"ui-grid-row-header-cell ui-grid-expandable-buttons-cell\"><div class=\"ui-grid-cell-contents\"><i ng-class=\"{ 'ui-grid-icon-plus-squared' : !row.isExpanded, 'ui-grid-icon-minus-squared' : row.isExpanded }\" ng-click=\"grid.api.expandable.toggleRowExpansion(row.entity)\"></i></div></div>"
26652 );
26653
26654
26655 $templateCache.put('ui-grid/expandableScrollFiller',
26656 "<div ng-if=\"expandableRow.shouldRenderFiller()\" ng-class=\"{scrollFiller:true, scrollFillerClass:(colContainer.name === 'body')}\" ng-style=\"{ width: (grid.getViewportWidth()) + 'px', height: row.expandedRowHeight + 2 + 'px', 'margin-left': grid.options.rowHeader.rowHeaderWidth + 'px' }\"><i class=\"ui-grid-icon-spin5 ui-grid-animate-spin\" ng-style=\"{'margin-top': ( row.expandedRowHeight/2 - 5) + 'px', 'margin-left' : ((grid.getViewportWidth() - grid.options.rowHeader.rowHeaderWidth)/2 - 5) + 'px'}\"></i></div>"
26657 );
26658
26659
26660 $templateCache.put('ui-grid/expandableTopRowHeader',
26661 "<div class=\"ui-grid-row-header-cell ui-grid-expandable-buttons-cell\"><div class=\"ui-grid-cell-contents\"><i ng-class=\"{ 'ui-grid-icon-plus-squared' : !grid.expandable.expandedAll, 'ui-grid-icon-minus-squared' : grid.expandable.expandedAll }\" ng-click=\"grid.api.expandable.toggleAllRows()\"></i></div></div>"
26662 );
26663
26664
26665 $templateCache.put('ui-grid/csvLink',
26666 "<span class=\"ui-grid-exporter-csv-link-span\"><a href=\"data:text/csv;charset=UTF-8,CSV_CONTENT\" download=\"FILE_NAME\">LINK_LABEL</a></span>"
26667 );
26668
26669
26670 $templateCache.put('ui-grid/importerMenuItem',
26671 "<li class=\"ui-grid-menu-item\"><form><input class=\"ui-grid-importer-file-chooser\" type=\"file\" id=\"files\" name=\"files[]\"></form></li>"
26672 );
26673
26674
26675 $templateCache.put('ui-grid/importerMenuItemContainer',
26676 "<div ui-grid-importer-menu-item></div>"
26677 );
26678
26679
26680 $templateCache.put('ui-grid/pagination',
26681 "<div role=\"contentinfo\" class=\"ui-grid-pager-panel\" ui-grid-pager ng-show=\"grid.options.enablePaginationControls\"><div role=\"navigation\" class=\"ui-grid-pager-container\"><div role=\"menubar\" class=\"ui-grid-pager-control\"><button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-first\" ui-grid-one-bind-title=\"aria.pageToFirst\" ui-grid-one-bind-aria-label=\"aria.pageToFirst\" ng-click=\"pageFirstPageClick()\" ng-disabled=\"cantPageBackward()\"><div class=\"first-triangle\"><div class=\"first-bar\"></div></div></button> <button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-previous\" ui-grid-one-bind-title=\"aria.pageBack\" ui-grid-one-bind-aria-label=\"aria.pageBack\" ng-click=\"pagePreviousPageClick()\" ng-disabled=\"cantPageBackward()\"><div class=\"first-triangle prev-triangle\"></div></button> <input type=\"number\" ui-grid-one-bind-title=\"aria.pageSelected\" ui-grid-one-bind-aria-label=\"aria.pageSelected\" class=\"ui-grid-pager-control-input\" ng-model=\"grid.options.paginationCurrentPage\" min=\"1\" max=\"{{ paginationApi.getTotalPages() }}\" required> <span class=\"ui-grid-pager-max-pages-number\" ng-show=\"paginationApi.getTotalPages() > 0\"><abbr ui-grid-one-bind-title=\"paginationOf\">/</abbr> {{ paginationApi.getTotalPages() }}</span> <button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-next\" ui-grid-one-bind-title=\"aria.pageForward\" ui-grid-one-bind-aria-label=\"aria.pageForward\" ng-click=\"pageNextPageClick()\" ng-disabled=\"cantPageForward()\"><div class=\"last-triangle next-triangle\"></div></button> <button type=\"button\" role=\"menuitem\" class=\"ui-grid-pager-last\" ui-grid-one-bind-title=\"aria.pageToLast\" ui-grid-one-bind-aria-label=\"aria.pageToLast\" ng-click=\"pageLastPageClick()\" ng-disabled=\"cantPageToLast()\"><div class=\"last-triangle\"><div class=\"last-bar\"></div></div></button></div><div class=\"ui-grid-pager-row-count-picker\" ng-if=\"grid.options.paginationPageSizes.length > 1\"><select ui-grid-one-bind-aria-labelledby-grid=\"'items-per-page-label'\" ng-model=\"grid.options.paginationPageSize\" ng-options=\"o as o for o in grid.options.paginationPageSizes\"></select><span ui-grid-one-bind-id-grid=\"'items-per-page-label'\" class=\"ui-grid-pager-row-count-label\">&nbsp;{{sizesLabel}}</span></div><span ng-if=\"grid.options.paginationPageSizes.length <= 1\" class=\"ui-grid-pager-row-count-label\">{{grid.options.paginationPageSize}}&nbsp;{{sizesLabel}}</span></div><div class=\"ui-grid-pager-count-container\"><div class=\"ui-grid-pager-count\"><span ng-show=\"grid.options.totalItems > 0\">{{showingLow}} <abbr ui-grid-one-bind-title=\"paginationThrough\">-</abbr> {{showingHigh}} {{paginationOf}} {{grid.options.totalItems}} {{totalItemsLabel}}</span></div></div></div>"
26682 );
26683
26684
26685 $templateCache.put('ui-grid/columnResizer',
26686 "<div ui-grid-column-resizer ng-if=\"grid.options.enableColumnResizing\" class=\"ui-grid-column-resizer\" col=\"col\" position=\"right\" render-index=\"renderIndex\" unselectable=\"on\"></div>"
26687 );
26688
26689
26690 $templateCache.put('ui-grid/gridFooterSelectedItems',
26691 "<span ng-if=\"grid.selection.selectedCount !== 0 && grid.options.enableFooterTotalSelected\">({{\"search.selectedItems\" | t}} {{grid.selection.selectedCount}})</span>"
26692 );
26693
26694
26695 $templateCache.put('ui-grid/selectionHeaderCell',
26696 "<div><!-- <div class=\"ui-grid-vertical-bar\">&nbsp;</div> --><div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><ui-grid-selection-select-all-buttons ng-if=\"grid.options.enableSelectAll\"></ui-grid-selection-select-all-buttons></div></div>"
26697 );
26698
26699
26700 $templateCache.put('ui-grid/selectionRowHeader',
26701 "<div class=\"ui-grid-disable-selection\"><div class=\"ui-grid-cell-contents\"><ui-grid-selection-row-header-buttons></ui-grid-selection-row-header-buttons></div></div>"
26702 );
26703
26704
26705 $templateCache.put('ui-grid/selectionRowHeaderButtons',
26706 "<div class=\"ui-grid-selection-row-header-buttons ui-grid-icon-ok\" ng-class=\"{'ui-grid-row-selected': row.isSelected}\" ng-click=\"selectButtonClick(row, $event)\">&nbsp;</div>"
26707 );
26708
26709
26710 $templateCache.put('ui-grid/selectionSelectAllButtons',
26711 "<div class=\"ui-grid-selection-row-header-buttons ui-grid-icon-ok\" ng-class=\"{'ui-grid-all-selected': grid.selection.selectAll}\" ng-click=\"headerButtonClick($event)\"></div>"
26712 );
26713
26714
26715 $templateCache.put('ui-grid/treeBaseExpandAllButtons',
26716 "<div class=\"ui-grid-tree-base-row-header-buttons\" ng-class=\"{'ui-grid-icon-minus-squared': grid.treeBase.numberLevels > 0 && grid.treeBase.expandAll, 'ui-grid-icon-plus-squared': grid.treeBase.numberLevels > 0 && !grid.treeBase.expandAll}\" ng-click=\"headerButtonClick($event)\"></div>"
26717 );
26718
26719
26720 $templateCache.put('ui-grid/treeBaseHeaderCell',
26721 "<div><div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\"><ui-grid-tree-base-expand-all-buttons></ui-grid-tree-base-expand-all-buttons></div></div>"
26722 );
26723
26724
26725 $templateCache.put('ui-grid/treeBaseRowHeader',
26726 "<div class=\"ui-grid-cell-contents\"><ui-grid-tree-base-row-header-buttons></ui-grid-tree-base-row-header-buttons></div>"
26727 );
26728
26729
26730 $templateCache.put('ui-grid/treeBaseRowHeaderButtons',
26731 "<div class=\"ui-grid-tree-base-row-header-buttons\" ng-class=\"{'ui-grid-tree-base-header': row.treeLevel > -1 }\" ng-click=\"treeButtonClick(row, $event)\"><i ng-class=\"{'ui-grid-icon-minus-squared': ( ( grid.options.showTreeExpandNoChildren && row.treeLevel > -1 ) || ( row.treeNode.children && row.treeNode.children.length > 0 ) ) && row.treeNode.state === 'expanded', 'ui-grid-icon-plus-squared': ( ( grid.options.showTreeExpandNoChildren && row.treeLevel > -1 ) || ( row.treeNode.children && row.treeNode.children.length > 0 ) ) && row.treeNode.state === 'collapsed'}\" ng-style=\"{'padding-left': grid.options.treeIndent * row.treeLevel + 'px'}\"></i> &nbsp;</div>"
26732 );
26733
26734 }]);
77 async: false
88 });
99
10 var faradayApp = angular.module('faradayApp', ['ngRoute', 'selectionModel', 'ui.bootstrap', 'angularFileUpload', 'filter', 'ngClipboard', 'ngCookies', 'cfp.hotkeys', 'chart.js'])
10 var faradayApp = angular.module('faradayApp', ['ngRoute', 'selectionModel', 'ui.bootstrap', 'angularFileUpload', 'filter', 'ngClipboard', 'ngCookies', 'cfp.hotkeys', 'chart.js', 'ui.grid', 'ui.grid.selection', 'ui.grid.grouping', 'ngSanitize', 'ui.grid.pagination', 'ui.grid.pinning'])
1111 .constant("BASEURL", (function() {
1212 var url = window.location.origin + "/";
1313 return url;
9292 controller: 'hostCtrl',
9393 title: 'Services | '
9494 }).
95 when('/status/ws/:wsId/groupby/:groupbyId', {
96 templateUrl: 'scripts/statusReport/partials/statusReport.html',
97 controller: 'statusReportCtrl',
98 title: 'Status Report | '
99 }).
100 when('/status/ws/:wsId/groupby/:groupbyId/search/:search', {
101 templateUrl: 'scripts/statusReport/partials/statusReport.html',
102 controller: 'statusReportCtrl',
103 title: 'Status Report | '
104 }).
105 when('/status/ws/:wsId/groupby/:groupbyId/search', {
106 templateUrl: 'scripts/statusReport/partials/statusReport.html',
107 controller: 'statusReportCtrl',
108 title: 'Status Report | '
109 }).
95110 when('/status/ws/:wsId/search/:search', {
96111 templateUrl: 'scripts/statusReport/partials/statusReport.html',
97112 controller: 'statusReportCtrl',
88 <div class="modal-body">
99 <div class="form-group">
1010 <h5>Search CWE database by typing in the field below</h5>
11 <input type="text" ng-model="cwe_selected" class="form-control input-sm" placeholder="Search for CWE" typeahead="cwe as cwe.name for cwe in cweList | filter:{name: $viewValue} | limitTo:10" typeahead-on-select="populate($item, $model, $label)">
11 <input type="text" ng-model="cwe_selected" class="form-control input-sm" placeholder="Search for CWE" uib-typeahead="cwe as cwe.name for cwe in cweList | filter:{name: $viewValue} | limitTo:10" typeahead-on-select="populate($item, $model, $label)">
1212 </div>
1313 <div class="form-group">
1414 <h5>Name</h5>
88 <div class="form-group">
99 <div class="col-md-12 form-group severities">
1010 <button type="button" class="btn btn-default dropdown-toggle color-{{data.property}}" data-toggle="dropdown" title="Change severity">
11 {{data.property || 'Edit severity'}} <span class="caret"></span>
11 {{data.property || 'Edit property'}} <span class="caret"></span>
1212 </button>
1313 <ul id="nav" class="dropdown-menu dropdown-menu-left" role="menu">
1414 <li ng-repeat="o in options"><a href="" class="ws color-{{o}}" ng-click="data.property = o">{{o}}</a></li>
114114 return xScale(d.x);
115115 })
116116 .attr('tooltip-append-to-body', true)
117 .attr('tooltip', function(d) {
117 .attr('uib-tooltip', function(d) {
118118 return d.k + " sums $" + d.x;
119119 })
120120 .attr('class', function(d) {
33
44 angular.module('faradayApp')
55 .controller('summarizedCtrl',
6 ['$scope', '$route', '$routeParams', '$modal', 'dashboardSrv', 'vulnsManager', 'workspacesFact',
7 function($scope, $route, $routeParams, $modal, dashboardSrv, vulnsManager, workspacesFact) {
6 ['$scope', '$route', '$routeParams', '$uibModal', '$filter', '$cookies', 'dashboardSrv', 'vulnsManager', 'workspacesFact',
7 function($scope, $route, $routeParams, $uibModal, $filter, $cookies, dashboardSrv, vulnsManager, workspacesFact) {
88 //current workspace
99 var workspace = $routeParams.wsId;
1010 $scope.servicesCount = [];
1616 $scope.pageSize = 10;
1717 $scope.pagination = 10;
1818 $scope.vulns;
19 $scope._areConfirmed = false;
20 var allVulns;
1921
2022 // graphicsBarCtrl data
2123 $scope.topServices; // previously known as treemapData
3335 $scope.wsStart;
3436 $scope.wsProgress;
3537
36 // cmd table sorting
37 $scope.cmdSortField = 'date';
38 $scope.cmdSortReverse = true;
38
39 init = function(vulns) {
40 // cmd table sorting
41 $scope.cmdSortField = 'date';
42 $scope.cmdSortReverse = true;
43 // host table sorting
44 $scope.hostSortField = 'name';
45 $scope.hostSortReverse = true;
46 // vuln table sorting
47 $scope.vulnSortField = 'metadata.create_time';
48 $scope.vulnSortReverse = true;
49
50 if($cookies.get('confirmed') === 'true') $scope._areConfirmed = true;
51
52 if(workspace != undefined) {
53 $scope.workspace = workspace;
54
55 dashboardSrv.getServicesCount(workspace).then(function(res) {
56 res.sort(function(a, b) {
57 return b.value - a.value;
58 });
59
60 $scope.servicesCount = res;
61
62 if(res.length > 4) {
63 var colors = ["#FA5882", "#FF0040", "#B40431", "#610B21", "#2A0A1B"];
64 var tmp = [];
65 res.slice(0, 5).forEach(function(srv) {
66 srv.color = colors.shift();
67 tmp.push(srv);
68 });
69 $scope.topServices = {"children": tmp};
70 }
71 });
72
73 dashboardSrv.getObjectsCount(workspace).then(function(res){
74 for(var i = res.length - 1; i >= 0; i--) {
75 if(res[i].key === "interfaces") {
76 res.splice(i, 1);
77 }
78 }
79 $scope.objectsCount = res;
80 });
81
82 // dashboardSrv.getVulnerabilitiesCount(workspace).then(function(res) {
83 // console.log(res);
84 // });
85
86 createGraphics = function(res) {
87 if(res.length > 0) {
88 var tmp = [
89 {"key": "critical", "value": 0, "color": "#932EBE", "amount": 0},
90 {"key": "high", "value": 0, "color": "#DF3936", "amount": 0},
91 {"key": "med", "value": 0, "color": "#DFBF35", "amount": 0},
92 {"key": "low", "value": 0, "color": "#A1CE31", "amount": 0},
93 {"key": "info", "value": 0, "color": "#428BCA", "amount": 0},
94 {"key": "unclassified", "value": 0, "color": "#999999", "amount": 0}
95 ];
96
97 res.forEach(function(tvuln) {
98 if(tvuln.key == 1 || tvuln.key == "info") {
99 dashboardSrv.accumulate(tmp, "info", tvuln.value, "value");
100 } else if (tvuln.key == 2 || tvuln.key == "low") {
101 dashboardSrv.accumulate(tmp, "low", tvuln.value, "value");
102 } else if (tvuln.key == 3 || tvuln.key == "med") {
103 dashboardSrv.accumulate(tmp, "med", tvuln.value, "value");
104 } else if (tvuln.key == 4 || tvuln.key == "high") {
105 dashboardSrv.accumulate(tmp, "high", tvuln.value, "value");
106 } else if (tvuln.key == 5 || tvuln.key == "critical") {
107 dashboardSrv.accumulate(tmp, "critical", tvuln.value, "value");
108 } else if (tvuln.key == 6 || tvuln.key == "unclassified") {
109 dashboardSrv.accumulate(tmp, "unclassified", tvuln.value, "value");
110 }
111 });
112
113 // used to create colored boxes for vulns
114 $scope.vulnsCount = tmp;
115
116 // used to create workspace's worth
117 $scope.generateVulnPrices($scope.vulnsCount, $scope.vulnPrices);
118 $scope.workspaceWorth = $scope.sumProperty($scope.vulnsCount, "amount");
119
120 // used to create pie chart for vulns
121 $scope.vulnsCountClass = {"children": angular.copy(tmp)};
122 $scope.doughnut = {key: [], value: [], colors: [], options: {maintainAspectRatio: false}};
123 for(var i = 0; i < $scope.vulnsCountClass.children.length; i++) {
124 if($scope.vulnsCountClass.children[i].key == "unclassified") {
125 $scope.vulnsCountClass.children.splice(i, 1);
126 break;
127 }
128 if($scope.vulnsCountClass.children[i].value !== 0) {
129 $scope.doughnut.key.push($scope.vulnsCountClass.children[i].key);
130 $scope.doughnut.value.push($scope.vulnsCountClass.children[i].value);
131 $scope.doughnut.colors.push($scope.vulnsCountClass.children[i].color);
132 }
133 };
134
135 $scope.$watch('vulnPrices', function(ps) {
136 if($scope.vulnsCount != undefined) {
137 $scope.generateVulnPrices($scope.vulnsCount, $scope.vulnPrices);
138 $scope.workspaceWorth = $scope.sumProperty($scope.vulnsCount, "amount");
139 }
140 }, true);
141 }
142 };
143
144
145 dashboardSrv.getCommands(workspace).then(function(res){
146 res.forEach(function(cmd){
147 cmd.user = cmd.user || "unknown";
148 cmd.hostname = cmd.hostname || "unknown";
149 cmd.ip = cmd.ip || "0.0.0.0";
150 if(cmd.duration == "0" || cmd.duration == "") {
151 cmd.duration = "In progress";
152 } else if (cmd.duration != undefined) {
153 cmd.duration = cmd.duration.toFixed(2) + "s";
154 }
155 cmd.date = cmd.startdate * 1000;
156 });
157 $scope.commands = res;
158 });
159
160 dashboardSrv.getHosts(workspace).then(function(res){
161 dashboardSrv.getHostsByServicesCount(workspace).then(function(servicesCount) {
162 res.forEach(function(host){
163 // Maybe this part should be in the view somehow
164 // or, even better, in CSS file
165 oss = ["windows", "cisco", "router", "osx", "apple","linux", "unix"];
166 oss.forEach(function(os) {
167 if (host.os.toLowerCase().indexOf(os) != -1) {
168 host.icon = os;
169 if (os == "unix") {
170 host.icon = "linux";
171 }else if (os == "apple") {
172 host.icon = "osx";
173 }
174 }
175 });
176 host.servicesCount = 0;
177 servicesCount.forEach(function(count){
178 if (count.key == host.id) {
179 host.servicesCount = count.value;
180 return
181 }
182 });
183 // load data for Top Hosts
184 if(servicesCount.length > 2) {
185 servicesCount.sort(function(a, b) {
186 return b.value-a.value;
187 });
188 var colors = ["rgb(57, 59, 121)","rgb(82, 84, 163)","rgb(107, 110, 207)"];
189 var tmp = {key:[], colors:[], value:[]};
190 tmp.options = {
191 showScale : false,
192 maintainAspectRatio: false
193 };
194 servicesCount.slice(0, 3).forEach(function(srv) {
195 tmp.colors.push(colors.shift());
196 tmp.value.push(srv.value);
197 tmp.key.push(host.name);
198 });
199 $scope.topHosts = tmp;
200 }
201 $scope.hosts.push(host);
202 });
203 });
204 });
205
206 vulnsManager.getVulns(workspace).then(function(vulns) {
207 confirmed_filter = { "confirmed":true };
208 filteredVulns = $filter('filter')(vulnsManager.vulns, confirmed_filter);
209
210 if($scope._areConfirmed === true) {
211 $scope.vulns = filteredVulns;
212 } else {
213 $scope.vulns = vulnsManager.vulns;
214 }
215
216 var data = angular.copy($scope.vulns);
217 var arrayVulnsParsed = vulnParse(data);
218 createGraphics(arrayVulnsParsed[0]);
219
220 for(key in arrayVulnsParsed[1]) {
221 if(arrayVulnsParsed[1].hasOwnProperty(key)) {
222 $scope.objectsCount.push({"key": key, "value": arrayVulnsParsed[1][key]});
223 }
224 }
225 });
226
227 workspacesFact.getDuration($scope.workspace).then(function(duration) {
228 $scope.wsDuration = duration;
229 $scope.wsProgress = $scope.calculateProgress($scope.wsDuration);
230 $scope.wsStart = $scope.wsDuration.start;
231 $scope.wsEnd = $scope.wsDuration.end;
232 });
233 }
234
235 };
236
237 vulnParse = function(vulns, confirmed) {
238 var arraySeverities = [];
239 var vulnsStatus = {
240 "web vulns": 0,
241 "vulns": 0,
242 "total vulns":0
243 };
244 var severity = {
245 "critical":0,
246 "high":0,
247 "med":0,
248 "low":0,
249 "info":0,
250 "unclassified":0
251 };
252 vulns.forEach(function(v) {
253 // Vulns Counter if need to do it
254 if(v.type === "VulnerabilityWeb") {
255 vulnsStatus["web vulns"] += 1;
256 } else {
257 vulnsStatus["vulns"] += 1;
258 }
259 vulnsStatus["total vulns"] += 1;
260
261 severity[v.severity] += 1;
262 });
263 for(key in severity) {
264 if(severity.hasOwnProperty(key)) {
265 arraySeverities.push({key:key,value:severity[key]});
266 }
267 }
268 return [arraySeverities, vulnsStatus];
269 };
270
271 $scope.getFilteredVulns = function() {
272 if($scope._areConfirmed === false) {
273 var data = angular.copy(filteredVulns);
274 var arrayVulnsParsed = vulnParse(data);
275 createGraphics(arrayVulnsParsed[0]);
276
277 $scope.objectsCount.forEach(function(obj) {
278 if(arrayVulnsParsed[1].hasOwnProperty(obj.key)) {
279 obj.value = arrayVulnsParsed[1][obj.key];
280 }
281 });
282 $scope._areConfirmed = true;
283 $cookies.put('confirmed', $scope._areConfirmed);
284 } else {
285 init();
286 $scope._areConfirmed = false;
287 $cookies.put('confirmed', $scope._areConfirmed);
288 }
289 };
290
39291 // toggles sort field and order
40292 $scope.cmdToggleSort = function(field) {
41293 $scope.cmdToggleSortField(field);
52304 $scope.cmdSortReverse = !$scope.cmdSortReverse;
53305 }
54306
55 // host table sorting
56 $scope.hostSortField = 'name';
57 $scope.hostSortReverse = true;
58307 // toggles sort field and order
59308 $scope.hostToggleSort = function(field) {
60309 $scope.hostToggleSortField(field);
71320 $scope.hostSortReverse = !$scope.hostSortReverse;
72321 }
73322
74 // vuln table sorting
75 $scope.vulnSortField = 'metadata.create_time';
76 $scope.vulnSortReverse = true;
77323 // toggles sort field and order
78324 $scope.vulnToggleSort = function(field) {
79325 $scope.vulnToggleSortField(field);
88334 // toggle column sort order
89335 $scope.vulnToggleReverse = function() {
90336 $scope.vulnSortReverse = !$scope.vulnSortReverse;
91 };
92
93 if(workspace != undefined) {
94 $scope.workspace = workspace;
95
96 dashboardSrv.getServicesCount(workspace).then(function(res) {
97 res.sort(function(a, b) {
98 return b.value - a.value;
99 });
100
101 $scope.servicesCount = res;
102
103 if(res.length > 4) {
104 var colors = ["#FA5882", "#FF0040", "#B40431", "#610B21", "#2A0A1B"];
105 var tmp = [];
106 res.slice(0, 5).forEach(function(srv) {
107 srv.color = colors.shift();
108 tmp.push(srv);
109 });
110 $scope.topServices = {"children": tmp};
111 }
112 });
113
114 dashboardSrv.getObjectsCount(workspace).then(function(res){
115 for(var i = res.length - 1; i >= 0; i--) {
116 if(res[i].key === "interfaces") {
117 res.splice(i, 1);
118 }
119 }
120 $scope.objectsCount = res;
121 });
122
123 dashboardSrv.getVulnerabilitiesCount(workspace).then(function(res) {
124 if(res.length > 0) {
125 var tmp = [
126 {"key": "critical", "value": 0, "color": "#932EBE", "amount": 0},
127 {"key": "high", "value": 0, "color": "#DF3936", "amount": 0},
128 {"key": "med", "value": 0, "color": "#DFBF35", "amount": 0},
129 {"key": "low", "value": 0, "color": "#A1CE31", "amount": 0},
130 {"key": "info", "value": 0, "color": "#428BCA", "amount": 0},
131 {"key": "unclassified", "value": 0, "color": "#999999", "amount": 0}
132 ];
133
134 res.forEach(function(tvuln) {
135 if(tvuln.key == 1 || tvuln.key == "info") {
136 dashboardSrv.accumulate(tmp, "info", tvuln.value, "value");
137 } else if (tvuln.key == 2 || tvuln.key == "low") {
138 dashboardSrv.accumulate(tmp, "low", tvuln.value, "value");
139 } else if (tvuln.key == 3 || tvuln.key == "med") {
140 dashboardSrv.accumulate(tmp, "med", tvuln.value, "value");
141 } else if (tvuln.key == 4 || tvuln.key == "high") {
142 dashboardSrv.accumulate(tmp, "high", tvuln.value, "value");
143 } else if (tvuln.key == 5 || tvuln.key == "critical") {
144 dashboardSrv.accumulate(tmp, "critical", tvuln.value, "value");
145 } else if (tvuln.key == 6 || tvuln.key == "unclassified") {
146 dashboardSrv.accumulate(tmp, "unclassified", tvuln.value, "value");
147 }
148 });
149
150 // used to create colored boxes for vulns
151 $scope.vulnsCount = tmp;
152
153 // used to create workspace's worth
154 $scope.generateVulnPrices($scope.vulnsCount, $scope.vulnPrices);
155 $scope.workspaceWorth = $scope.sumProperty($scope.vulnsCount, "amount");
156
157 // used to create pie chart for vulns
158 $scope.vulnsCountClass = {"children": angular.copy(tmp)};
159 $scope.doughnut = {key: [], value: [], colors: [], options: {maintainAspectRatio: false}};
160 for(var i = 0; i < $scope.vulnsCountClass.children.length; i++) {
161 if($scope.vulnsCountClass.children[i].key == "unclassified") {
162 $scope.vulnsCountClass.children.splice(i, 1);
163 break;
164 }
165 $scope.doughnut.key.push($scope.vulnsCountClass.children[i].key);
166 $scope.doughnut.value.push($scope.vulnsCountClass.children[i].value);
167 $scope.doughnut.colors.push($scope.vulnsCountClass.children[i].color);
168 };
169
170 $scope.$watch('vulnPrices', function(ps) {
171 if($scope.vulnsCount != undefined) {
172 $scope.generateVulnPrices($scope.vulnsCount, $scope.vulnPrices);
173 $scope.workspaceWorth = $scope.sumProperty($scope.vulnsCount, "amount");
174 }
175 }, true);
176 }
177 });
178
179 dashboardSrv.getCommands(workspace).then(function(res){
180 res.forEach(function(cmd){
181 cmd.user = cmd.user || "unknown";
182 cmd.hostname = cmd.hostname || "unknown";
183 cmd.ip = cmd.ip || "0.0.0.0";
184 if(cmd.duration == "0" || cmd.duration == "") {
185 cmd.duration = "In progress";
186 } else if (cmd.duration != undefined) {
187 cmd.duration = cmd.duration.toFixed(2) + "s";
188 }
189 cmd.date = cmd.startdate * 1000;
190 });
191 $scope.commands = res;
192 });
193
194 dashboardSrv.getHosts(workspace).then(function(res){
195 dashboardSrv.getHostsByServicesCount(workspace).then(function(servicesCount) {
196 res.forEach(function(host){
197 // Maybe this part should be in the view somehow
198 // or, even better, in CSS file
199 oss = ["windows", "cisco", "router", "osx", "apple","linux", "unix"];
200 oss.forEach(function(os) {
201 if (host.os.toLowerCase().indexOf(os) != -1) {
202 host.icon = os;
203 if (os == "unix") {
204 host.icon = "linux";
205 }else if (os == "apple") {
206 host.icon = "osx";
207 }
208 }
209 });
210 host.servicesCount = 0;
211 servicesCount.forEach(function(count){
212 if (count.key == host.id) {
213 host.servicesCount = count.value;
214 return
215 }
216 });
217 // load data for Top Hosts
218 if(servicesCount.length > 2) {
219 servicesCount.sort(function(a, b) {
220 return b.value-a.value;
221 });
222 var colors = ["rgb(57, 59, 121)","rgb(82, 84, 163)","rgb(107, 110, 207)"];
223 var tmp = {key:[], colors:[], value:[]};
224 tmp.options = {
225 showScale : false,
226 maintainAspectRatio: false
227 };
228 servicesCount.slice(0, 3).forEach(function(srv) {
229 tmp.colors.push(colors.shift());
230 tmp.value.push(srv.value);
231 tmp.key.push(host.name);
232 });
233 $scope.topHosts = tmp;
234 }
235 $scope.hosts.push(host);
236 });
237 });
238 });
239
240 vulnsManager.getVulns(workspace).then(function(vulns) {
241 $scope.vulns = vulns;
242 });
243
244 workspacesFact.getDuration($scope.workspace).then(function(duration) {
245 $scope.wsDuration = duration;
246 $scope.wsProgress = $scope.calculateProgress($scope.wsDuration);
247 $scope.wsStart = $scope.wsDuration.start;
248 $scope.wsEnd = $scope.wsDuration.end;
249 });
250337 };
251338
252339 $scope.numberOfPages = function() {
271358
272359 $scope.showServices = function(host) {
273360 if ($scope.workspace != undefined){
274 var modal = $modal.open({
361 var modal = $uibModal.open({
275362 templateUrl: 'scripts/dashboard/partials/modal-services-by-host.html',
276363 controller: 'summarizedCtrlServicesModal',
277364 size: 'lg',
289376
290377 $scope.showHosts = function(srv_name) {
291378 if ($scope.workspace != undefined){
292 var modal = $modal.open({
379 var modal = $uibModal.open({
293380 templateUrl: 'scripts/dashboard/partials/modal-hosts-by-service.html',
294381 controller: 'summarizedCtrlHostsModal',
295382 size: 'lg',
307394
308395 $scope.treemap = function(data) {
309396 if(data !== undefined && data != {}) {
310 var modal = $modal.open({
397 var modal = $uibModal.open({
311398 templateUrl: 'scripts/dashboard/partials/modal-treemap.html',
312399 controller: 'treemapModalCtrl',
313400 size: 'lg',
360447
361448 return progress;
362449 };
450
451 init();
363452 }]);
364453
365454 angular.module('faradayApp')
00 <article id='list' class='panel panel-default'>
11 <header>
22 <h2>Commands History
3 <span class="glyphicon glyphicon-info-sign" tooltip="Shows current WS' executed commands"></span>
3 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Shows current WS' executed commands"></span>
44 </h2>
55 </header>
66 <div ng-if="commands.length == 0" class="alert alert-info alert-dismissible no-margin-bottom">
2121 </thead>
2222 <tbody>
2323 <tr ng-repeat="cmd in commands | orderObjectBy:cmdSortField:cmdSortReverse">
24 <td><p tooltip="{{cmd.ip}}">{{cmd.user}}@{{cmd.hostname}}</p></td>
24 <td><p uib-tooltip="{{cmd.ip}}">{{cmd.user}}@{{cmd.hostname}}</p></td>
2525 <td class="wrapword">{{cmd.command}}</td>
2626 <td>{{cmd.date | date:"MM/dd/yyyy 'at' h:mma"}}</td>
2727 <td ng-bind="cmd.duration || 'undefined'"></td>
2828 </tr>
2929 </tbody>
3030 </table>
31 </article>
31 </article>
00 <article id="compound" class='panel panel-default'>
11 <header>
22 <h2><a href="#/hosts/ws/{{workspace}}">Hosts</a>
3 <span class="glyphicon glyphicon-info-sign" tooltip="All hosts, each one showing its service count and operating system. By clicking on a host IP you can access a list with all of its services"></span>
3 <span class="glyphicon glyphicon-info-sign" uib-tooltip="All hosts, each one showing its service count and operating system. By clicking on a host IP you can access a list with all of its services"></span>
44 </h2>
55 </header>
66 <div ng-if="hosts.length == 0" class="alert alert-info alert-dismissible no-margin-bottom">
2323 startFrom:currentPage*pageSize | limitTo:pageSize">
2424 <td class="col-xs-6">
2525 <a href="" class="host" ng-click="showServices(host)">{{host.name}}</a>
26 <a href="//www.shodan.io/search?query={{host.name}}" tooltip="Search in Shodan" target="_blank">
26 <a href="//www.shodan.io/search?query={{host.name}}" uib-tooltip="Search in Shodan" target="_blank">
2727 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
2828 </a>
2929 </td>
3030 <td class="col-xs-6">{{host.servicesCount}}</td>
3131 <td class="col-xs-4">
32 <img ng-if="host.icon != undefined" ng-src="../././reports/images/{{host.icon}}.png" tooltip="{{host.os}}"/>
33 <span ng-if="host.icon == undefined" class="fa fa-laptop" tooltip="{{host.os}}"></span>
32 <img ng-if="host.icon != undefined" ng-src="../././reports/images/{{host.icon}}.png" uib-tooltip="{{host.os}}"/>
33 <span ng-if="host.icon == undefined" class="fa fa-laptop" uib-tooltip="{{host.os}}"></span>
3434 </td>
3535 </tr>
3636 </tbody>
11 <!-- Copyright (C) 2013 Infobyte LLC (http://www.infobytesec.com/) -->
22 <!-- See the file 'doc/LICENSE' for the license information -->
33
4 <section id="main" class="seccion clearfix">
4 <section id="main" class="seccion clearfix" ng-controller="summarizedCtrl">
55 <div class="right-main">
66 <div id="reports-main" class="fila clearfix">
77 <h2 class="ws-label">
8 <span id="ws-name" title="Current workspace">Dashboard for {{ workspace }}</span><!-- WS name -->
8 <span id="ws-name" title="Current workspace">
9 Dashboard for {{ workspace }} ({{ _areConfirmed === false ? 'all vulns' : 'confirmed' }})
10 </span><!-- WS name -->
911 <div id="ws-control" class="btn-group">
1012 <button id="refresh" type="button" class="btn btn-danger" title="Refresh current workspace" ng-click="location.reload()">
1113 <span class="glyphicon glyphicon-refresh"></span>
1315 <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" title="Change current workspace">
1416 Change workspace <span class="caret"></span>
1517 </button>
16 <ul id="nav" class="dropdown-menu dropdown-menu-right" role="menu">
18 <ul id="nav" class="dropdown-menu dropdown-menu-left margin" role="menu">
1719 <li ng-repeat="ws in workspaces"><a href="#/dashboard/ws/{{ws}}" class="ws" >{{ws}}</a></li>
1820 </ul><!-- WS navigation -->
21 <button type="button" class="btn btn-danger vulns-filter" title="{{_areConfirmed === false ? 'View confirmed vulns' : 'View all vulns'}}" ng-click="getFilteredVulns()">
22 {{_areConfirmed === false ? 'View confirmed vulns' : 'View all vulns'}} <span class="glyphicon glyphicon-filter"></span>
23 </button>
1924 </div><!-- #ws-control -->
2025 </h2><!-- .ws-label -->
2126 <div class="reports">
2227 </div><!-- .reports -->
23 <div ng-controller="summarizedCtrl">
28 <div>
2429 <div ng-include="'scripts/dashboard/partials/graphics-bar.html'"></div>
2530 <div ng-include="'scripts/dashboard/partials/summarized.html'"></div>
2631 <div class="row">
66 <header>
77 <h2>
88 <a class="treemap-view" href="" ng-disabled="{{nodata}}" ng-click="treemap(topServices)">Top Services</a>
9 <span class="glyphicon glyphicon-info-sign" tooltip="Top 5 services with the largest quantity of hosts"></span>
9 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Top 5 services with the largest quantity of hosts"></span>
1010 </h2>
1111 </header>
1212 <span id="treemapText"></span>
2424 <article id='bar' class='panel panel-default'>
2525 <header>
2626 <h2>Top Hosts
27 <span class="glyphicon glyphicon-info-sign" tooltip="Top 3 hosts with the largest quantity of services"></span>
27 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Top 3 hosts with the largest quantity of services"></span>
2828 </h2>
2929 </header>
3030 <span id="barText"></span>
4242 <article id='cake' class='panel panel-default'>
4343 <header>
4444 <h2>Vulnerabilities
45 <span class="glyphicon glyphicon-info-sign" tooltip="Vulnerabilty distribution for current WS"></span>
45 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Vulnerabilty distribution for current WS"></span>
4646 </h2>
4747 </header>
4848 <span id="cakeText"></span>
49 <div ng-if="vulnsCountClass == undefined || vulnsCountClass.length == 0" class="alert alert-info alert-dismissible">
49 <div ng-if="vulnsCountClass == undefined || doughnut.value.length === 0" class="alert alert-info alert-dismissible">
5050 <button type="button" class="close" data-dismiss="alert">
5151 <span aria-hidden="true">&times;</span>
5252 <span class="sr-only">Close</span>
5353 </button>
5454 <p>No vulnerabilities found yet</p>
5555 </div>
56 <div id="doughnut" ng-include="'scripts/dashboard/partials/doughnut.html'" ng-if="vulnsCountClass != undefined || vulnsCountClass.length != 0"></div>
56 <div id="doughnut" ng-include="'scripts/dashboard/partials/doughnut.html'" ng-if="doughnut.value.length !== 0"></div>
5757 </article>
5858 </div>
00 <article class='panel panel-default left-big-box'>
11 <header>
22 <h2>Last Vulnerabilities
3 <span class="glyphicon glyphicon-info-sign" tooltip="Last vulnerabilities added"></span>
3 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Last vulnerabilities added"></span>
44 </h2>
55 </header>
66 <div ng-if="vulns.length == 0" class="alert alert-info alert-dismissible no-margin-bottom">
2626 <td>{{vuln.metadata.create_time * 1000 | date:"MM/dd/yyyy 'at' h:mma"}}</td>
2727 <td>
2828 {{vuln.target}}
29 <a href="//www.shodan.io/search?query={{vuln.target}}" tooltip="Search in Shodan" target="_blank">
29 <a href="//www.shodan.io/search?query={{vuln.target}}" uib-tooltip="Search in Shodan" target="_blank">
3030 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
3131 </a>
3232 </td>
4040 </tr>
4141 </tbody>
4242 </table>
43 </article>
43 </article>
1919 <td><input disabled type="checkbox" ng-model="host.owned"/></td>
2020 <td>
2121 {{host.name}}
22 <a href="//www.shodan.io/search?query={{host.name}}" tooltip="Search in Shodan" target="_blank">
22 <a href="//www.shodan.io/search?query={{host.name}}" uib-tooltip="Search in Shodan" target="_blank">
2323 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
2424 </a>
2525 </td>
2020 <tr ng-repeat="srv in services | orderBy:sortField:sortReverse">
2121 <td>
2222 {{srv.name}}
23 <a href="//www.shodan.io/search?query={{srv.name}}" tooltip="Search in Shodan" target="_blank">
23 <a href="//www.shodan.io/search?query={{srv.name}}" uib-tooltip="Search in Shodan" target="_blank">
2424 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
2525 </a>
2626 </td>
2828 <td>
2929 <li ng-repeat="p in srv.ports">
3030 {{p}}
31 <a href="//www.shodan.io/search?query=port:{{p}}" tooltip="Search in Shodan" target="_blank">
31 <a href="//www.shodan.io/search?query=port:{{p}}" uib-tooltip="Search in Shodan" target="_blank">
3232 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
3333 </a>
3434 </li>
66 <article class='panel panel-default'>
77 <header>
88 <h2>Services report
9 <span class="glyphicon glyphicon-info-sign" tooltip="All services for current WS ordered by host amount"></span>
9 <span class="glyphicon glyphicon-info-sign" uib-tooltip="All services for current WS ordered by host amount"></span>
1010 </h2>
1111 </header>
1212 <div ng-if="servicesCount.length == 0" class="alert alert-info alert-dismissible">
3838 <article class='panel panel-default'>
3939 <header>
4040 <h2>Workspace summarized report
41 <span class="glyphicon glyphicon-info-sign" tooltip="WS overview - hosts, notes, services and vulnerabilites counts"></span>
41 <span class="glyphicon glyphicon-info-sign" uib-tooltip="WS overview - hosts, notes, services and vulnerabilites counts"></span>
4242 </h2>
4343 </header>
4444 <div ng-if="objectsCount.length == 0" class="alert alert-info alert-dismissible">
11 <header>
22 <h2>
33 Workspace's worth
4 <span class="glyphicon glyphicon-info-sign" tooltip="Total net worth of Workspace, according to current vulnerabilities' prices"></span>
4 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Total net worth of Workspace, according to current vulnerabilities' prices"></span>
55 </h2>
66 </header>
77 <div ng-if="vulnsCount == undefined || vulnsCount.length == 0" class="alert alert-info alert-dismissible no-margin-bottom">
1616 <div d3-horizontal-stacked-bar data="vulnsCount" class="stackedbars"></div>
1717 <div id="vulns-by-price-reference" class="center-lg-6">
1818 <ul class="label-list">
19 <li ng-repeat="(severity, price) in vulnPrices" tooltip="Click on number to edit price">
19 <li ng-repeat="(severity, price) in vulnPrices" uib-tooltip="Click on number to edit price">
2020 <span class="label vuln fondo-{{severity}}">
2121 {{severity}} $
2222 <span contenteditable="true" ng-model="vulnPrices[severity]"></span>
11 <header>
22 <h2>
33 <a href="../././reports/index.html#/status/ws/{{workspace}}" class="status-report">Vulnerabilities</a>
4 <span class="glyphicon glyphicon-info-sign" tooltip="Vulnerabilities count arranged by severity"></span>
4 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Vulnerabilities count arranged by severity"></span>
55 </h2>
66 </header>
77 <div ng-if="vulnsCount == undefined || vulnsCount.length == 0" class="alert alert-info alert-dismissible no-margin-bottom">
11 <header>
22 <h2>
33 Workspace progress
4 <span class="glyphicon glyphicon-info-sign" tooltip="Workspace progress according to Scope dates"></span>
4 <span class="glyphicon glyphicon-info-sign" uib-tooltip="Workspace progress according to Scope dates"></span>
55 </h2>
66 </header>
77 <div>
88 <div id="workspace-progress-reference" class="center-lg-6" ng-if="wsProgress">
9 <progressbar value="wsProgress" class="progress-striped">{{wsProgress}}%</progressbar>
9 <uib-progressbar value="wsProgress" class="progress-striped">{{wsProgress}}%</uib-progressbar>
1010 <ul class="label-list">
1111 <li><span class="label label-default">Start date: {{wsStart | date:"MM/dd/yyyy"}}</span></li>
1212 <li><span class="label label-default">End date: {{wsEnd | date:"MM/dd/yyyy"}}</span></li>
4141 };
4242
4343 dashboardSrv.getVulnerabilities = function(ws) {
44 var deferred = $q.defer();
45
4446 var url = BASEURL + "/" + ws + "/_design/vulns/_view/all";
45 return dashboardSrv._getView(url);
47 var AllVulns = [];
48 dashboardSrv._getView(url).then(function(vulns) {
49 vulns.forEach(function(v) {
50 if(v.confirmed === true) AllVulns.push(v);
51 });
52 deferred.resolve(AllVulns);
53 });
54 return deferred.promise;
4655 };
4756
4857 dashboardSrv.getVulnerabilitiesCount = function(ws) {
33
44 angular.module('faradayApp')
55 .controller('hostCtrl',
6 ['$scope', '$cookies', '$filter', '$location', '$route', '$routeParams', '$modal', 'hostsManager', 'workspacesFact', 'dashboardSrv', 'servicesManager',
7 function($scope, $cookies, $filter, $location, $route, $routeParams, $modal, hostsManager, workspacesFact, dashboardSrv, servicesManager) {
6 ['$scope', '$cookies', '$filter', '$location', '$route', '$routeParams', '$uibModal', 'hostsManager', 'workspacesFact', 'dashboardSrv', 'servicesManager',
7 function($scope, $cookies, $filter, $location, $route, $routeParams, $uibModal, hostsManager, workspacesFact, dashboardSrv, servicesManager) {
88
99 init = function() {
1010 $scope.selectall = false;
171171 };
172172
173173 $scope.new = function() {
174 var modal = $modal.open({
174 var modal = $uibModal.open({
175175 templateUrl: 'scripts/services/partials/modalNew.html',
176176 controller: 'serviceModalNew',
177177 size: 'lg',
191191 servicesManager.createService(service, $scope.workspace).then(function(service) {
192192 $scope.services.push(service);
193193 }, function(message) {
194 $modal.open(config = {
194 $uibModal.open(config = {
195195 templateUrl: 'scripts/commons/partials/modalKO.html',
196196 controller: 'commonsModalKoCtrl',
197197 size: 'sm',
216216
217217 $scope.edit = function() {
218218 if($scope.selectedServices().length > 0) {
219 var modal = $modal.open({
219 var modal = $uibModal.open({
220220 templateUrl: 'scripts/services/partials/modalEdit.html',
221221 controller: 'serviceModalEdit',
222222 size: 'lg',
234234 $scope.update($scope.selectedServices(), data);
235235 });
236236 } else {
237 $modal.open(config = {
237 $uibModal.open(config = {
238238 templateUrl: 'scripts/commons/partials/modalKO.html',
239239 controller: 'commonsModalKoCtrl',
240240 size: 'sm',
256256 });
257257
258258 if(selected.length == 0) {
259 $modal.open(config = {
259 $uibModal.open(config = {
260260 templateUrl: 'scripts/commons/partials/modalKO.html',
261261 controller: 'commonsModalKoCtrl',
262262 size: 'sm',
272272 message = selected.length + " hosts will be deleted";
273273 }
274274 message = message.concat(" along with all of its children. This operation cannot be undone. Are you sure you want to proceed?");
275 $modal.open(config = {
275 $uibModal.open(config = {
276276 templateUrl: 'scripts/commons/partials/modalDelete.html',
277277 controller: 'commonsModalDelete',
278278 size: 'lg',
33
44 angular.module('faradayApp')
55 .controller('hostsCtrl',
6 ['$scope', '$cookies', '$filter', '$location', '$route', '$routeParams', '$modal', 'hostsManager', 'workspacesFact',
7 function($scope, $cookies, $filter, $location, $route, $routeParams, $modal, hostsManager, workspacesFact) {
6 ['$scope', '$cookies', '$filter', '$location', '$route', '$routeParams', '$uibModal', 'hostsManager', 'workspacesFact',
7 function($scope, $cookies, $filter, $location, $route, $routeParams, $uibModal, hostsManager, workspacesFact) {
88
99 init = function() {
1010 $scope.selectall = false;
197197 });
198198
199199 if(selected.length == 0) {
200 $modal.open(config = {
200 $uibModal.open(config = {
201201 templateUrl: 'scripts/commons/partials/modalKO.html',
202202 controller: 'commonsModalKoCtrl',
203203 size: 'sm',
213213 message = selected.length + " hosts will be deleted";
214214 }
215215 message = message.concat(" along with all of its children. This operation cannot be undone. Are you sure you want to proceed?");
216 $modal.open(config = {
216 $uibModal.open(config = {
217217 templateUrl: 'scripts/commons/partials/modalDelete.html',
218218 controller: 'commonsModalDelete',
219219 size: 'lg',
236236 $scope.hosts.push(host);
237237 $scope.loadIcons();
238238 }, function(message) {
239 $modal.open(config = {
239 $uibModal.open(config = {
240240 templateUrl: 'scripts/commons/partials/modalKO.html',
241241 controller: 'commonsModalKoCtrl',
242242 size: 'sm',
250250 }
251251
252252 $scope.new = function() {
253 var modal = $modal.open({
253 var modal = $uibModal.open({
254254 templateUrl: 'scripts/hosts/partials/modalNew.html',
255255 controller: 'hostsModalNew',
256256 size: 'lg',
276276
277277 $scope.edit = function() {
278278 if($scope.selectedHosts().length == 1) {
279 var modal = $modal.open({
279 var modal = $uibModal.open({
280280 templateUrl: 'scripts/hosts/partials/modalEdit.html',
281281 controller: 'hostsModalEdit',
282282 size: 'lg',
293293 $scope.update($scope.selectedHosts()[0], hostdata, interfaceData);
294294 });
295295 } else {
296 $modal.open(config = {
296 $uibModal.open(config = {
297297 templateUrl: 'scripts/commons/partials/modalKO.html',
298298 controller: 'commonsModalKoCtrl',
299299 size: 'sm',
8080 <td><a href="#/host/ws/{{workspace}}/hid/{{host._id}}">View</a></td>
8181 <td>
8282 {{host.name}}
83 <a href="//www.shodan.io/search?query={{host.name}}" tooltip="Search in Shodan" target="_blank">
83 <a href="//www.shodan.io/search?query={{host.name}}" uib-tooltip="Search in Shodan" target="_blank">
8484 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
8585 </a>
8686 </td>
8787 <td ng-bind="host.description || '-'"></td>
8888 <td> <a ng-href="#/status/ws/{{workspace}}/search/target={{host.name}}" ng-bind="vulnsCount[host._id] || '-'"></a></td>
8989 <td>
90 <img ng-if="host.icon != undefined" ng-src="../././reports/images/{{host.icon}}.png" tooltip="{{host.os}}"/>
91 <span ng-if="host.icon == undefined" class="fa fa-laptop" tooltip="{{host.os}}"></span>
90 <img ng-if="host.icon != undefined" ng-src="../././reports/images/{{host.icon}}.png" uib-tooltip="{{host.os}}"/>
91 <span ng-if="host.icon == undefined" class="fa fa-laptop" uib-tooltip="{{host.os}}"></span>
9292 </td>
9393 <td ng-bind="host.owned"></td>
9494 </tr>
3232 $scope.updateWorkspace = function() {
3333 if($routeParams.wsId != undefined) {
3434 $scope.workspace = $routeParams.wsId;
35 $cookies.currentUrl = $location.path();
35 $cookies.put('currentUrl', $location.path());
3636 }
3737 };
3838
4242 } else {
4343 $scope.component = $location.path().split("/")[1];
4444 }
45 $cookies.currentComponent = $scope.component;
45 $cookies.put('currentComponent', $scope.component);
4646 };
4747
4848 $scope.showNavigation = function() {
5353 $scope.loadCurrentWorkspace = function() {
5454 var pos = -1;
5555
56 if($cookies.currentUrl != undefined) {
57 pos = $cookies.currentUrl.indexOf('ws/');
56 if($cookies.get('currentUrl') != undefined) {
57 pos = $cookies.get('currentUrl').indexOf('ws/');
5858 }
5959
6060 if($routeParams.wsId != undefined) {
6161 $scope.workspace = $routeParams.wsId;
6262 } else if(pos >= 0) {
63 $scope.workspace = $cookies.currentUrl.slice(pos+3);
63 $scope.workspace = $cookies.get('currentUrl').slice(pos+3);
6464 }
6565 };
6666
44 <nav>
55 <ul>
66 <li>
7 <a href="#/dashboard/ws/{{workspace}}" class="ws-dashboard" style="color: #ffffff !important" tooltip="Dashboard" tooltip-placement="right">
7 <a href="#/dashboard/ws/{{workspace}}" class="ws-dashboard" style="color: #ffffff !important" uib-tooltip="Dashboard" tooltip-placement="right">
88 <img src="images/ico-dashboard-menu.svg" alt="Dashboard"/>
99 </a>
1010 </li>
1111 <li>
12 <a href="#/status/ws/{{workspace}}" class="status-report" style="color: #ffffff !important" tooltip="Status Report" tooltip-placement="right">
12 <a href="#/status/ws/{{workspace}}" class="status-report" style="color: #ffffff !important" uib-tooltip="Status Report" tooltip-placement="right">
1313 <img src="images/ico-status-menu.svg" alt="Status Report"/>
1414 </a>
1515 </li>
1616 <li>
17 <a href="#/workspaces" class="workspaces" style="color: #ffffff !important" tooltip="Workspaces" tooltip-placement="right">
17 <a href="#/workspaces" class="workspaces" style="color: #ffffff !important" uib-tooltip="Workspaces" tooltip-placement="right">
1818 <img src="images/ico-workspaces-menu.svg" alt="Workspaces"/>
1919 </a>
2020 </li>
2121 <li>
22 <a href="#/hosts/ws/{{workspace}}" class="workspaces" style="color: #ffffff !important" tooltip="Hosts" tooltip-placement="right">
22 <a href="#/hosts/ws/{{workspace}}" class="workspaces" style="color: #ffffff !important" uib-tooltip="Hosts" tooltip-placement="right">
2323 <i class="fa fa-sitemap host"></i>
2424 </a>
2525 </li>
2626 <li>
27 <a href="#/users" class="users" style="color: #ffffff !important" tooltip="Users" tooltip-placement="right">
27 <a href="#/users" class="users" style="color: #ffffff !important" uib-tooltip="Users" tooltip-placement="right">
2828 <img src="images/ico-users-menu.svg" alt="Users"/>
2929 </a>
3030 </li>
3131 <li>
32 <a href="#/executive" class="executive-report" style="color: #ffffff !important" tooltip="Executive Report" tooltip-placement="right">
32 <a href="#/executive" class="executive-report" style="color: #ffffff !important" uib-tooltip="Executive Report" tooltip-placement="right">
3333 <img src="images/ico-executive-menu.svg" alt="Executive Report"/>
3434 </a>
3535 </li>
3636 <li>
37 <a href="#/communication" class="executive-report" style="color: #ffffff !important" tooltip="Chat" tooltip-placement="right">
37 <a href="#/communication" class="executive-report" style="color: #ffffff !important" uib-tooltip="Chat" tooltip-placement="right">
3838 <img src="images/ico-communication-menu.svg" alt="Communication"/>
3939 </a>
4040 </li>
4141 <li>
42 <a href="#/comparison" class="executive-report" style="color: #ffffff !important" tooltip="Workspaces Comparison" tooltip-placement="right">
42 <a href="#/comparison" class="executive-report" style="color: #ffffff !important" uib-tooltip="Workspaces Comparison" tooltip-placement="right">
4343 <img src="images/ico-workspace-comparison-menu.svg" alt="Workspaces Comparison"/>
4444 </a>
4545 </li>
4646 <li>
47 <a href="#/webshell" class="executive-report" style="color: #ffffff !important" tooltip="Web Shell" tooltip-placement="right">
47 <a href="#/webshell" class="executive-report" style="color: #ffffff !important" uib-tooltip="Web Shell" tooltip-placement="right">
4848 <img src="images/ico-web-shell-menu.svg" alt="Webshell"/>
4949 </a>
5050 </li>
101101 <td><input type="checkbox" name="{{s._id}}"/></td>
102102 <td>
103103 {{service.name}}
104 <a href="//www.shodan.io/search?query={{service.name}}" tooltip="Search in Shodan" target="_blank">
104 <a href="//www.shodan.io/search?query={{service.name}}" uib-tooltip="Search in Shodan" target="_blank">
105105 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
106106 </a>
107107 </td>
109109 <td>
110110 <li ng-repeat="p in service.ports">
111111 {{p}}
112 <a href="//www.shodan.io/search?query=port:{{p}}" tooltip="Search in Shodan" target="_blank">
112 <a href="//www.shodan.io/search?query=port:{{p}}" uib-tooltip="Search in Shodan" target="_blank">
113113 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
114114 </a>
115115 </li>
3636
3737 vm.data = {
3838 _attachments: {},
39 confirmed: false,
3940 data: "",
4041 desc: "",
4142 easeofresolution: undefined,
44 angular.module('faradayApp')
55 .controller('statusReportCtrl',
66 ['$scope', '$filter', '$routeParams',
7 '$location', '$modal', '$cookies', '$q', '$window', 'BASEURL',
7 '$location', '$uibModal', '$cookies', '$q', '$window', 'BASEURL',
88 'SEVERITIES', 'EASEOFRESOLUTION', 'hostsManager',
9 'vulnsManager', 'workspacesFact', 'csvService',
9 'vulnsManager', 'workspacesFact', 'csvService', 'uiGridConstants',
1010 function($scope, $filter, $routeParams,
11 $location, $modal, $cookies, $q, $window, BASEURL,
11 $location, $uibModal, $cookies, $q, $window, BASEURL,
1212 SEVERITIES, EASEOFRESOLUTION, hostsManager,
13 vulnsManager, workspacesFact, csvService) {
13 vulnsManager, workspacesFact, csvService, uiGridConstants) {
1414 $scope.baseurl;
1515 $scope.columns;
1616 $scope.easeofresolution;
2525 $scope.workspaces;
2626 $scope.currentPage;
2727 $scope.newCurrentPage;
28 $scope.pageSize;
2928 $scope.newPageSize;
29 $scope.gridOptions;
3030
3131 $scope.vulnWebSelected;
32 $scope.confirmed = false;
33 var allVulns;
3234
3335 init = function() {
3436 $scope.baseurl = BASEURL;
3537 $scope.severities = SEVERITIES;
3638 $scope.easeofresolution = EASEOFRESOLUTION;
39 $scope.propertyGroupBy = $routeParams.groupbyId;
3740 $scope.sortField = 'metadata.create_time';
41 $scope.colProperties = ["date","name","severity","service","target","desc","resolution","data","status","website","path","request","method","params","pname","query","response"];
3842 $scope.reverse = true;
3943 $scope.vulns = [];
40
41 $scope.pageSize = 10;
42 $scope.currentPage = 0;
43 $scope.newCurrentPage = 0;
44
45 if (!isNaN(parseInt($cookies.pageSize)))
46 $scope.pageSize = parseInt($cookies.pageSize);
47 $scope.newPageSize = $scope.pageSize;
44 $scope.selected = false;
45
46 var deleteRow = '<div ng-if="row.entity._id != undefined" class="ui-grid-cell-contents row-tooltip text-center">'+
47 '<span ng-click="grid.appScope.deleteVuln(row.entity)" class="glyphicon glyphicon-trash cursor" uib-tooltip="Delete"></span>'+
48 '</div>';
49 var editRow = '<div ng-if="row.entity._id != undefined" class="ui-grid-cell-contents row-tooltip text-center">'+
50 '<span ng-click="grid.appScope.editVuln(row.entity)" class="glyphicon glyphicon-pencil cursor" uib-tooltip="Edit"></span>'+
51 '</div>';
52
53 var filterRow = '<div ng-if="row.entity._id != undefined" class="ui-grid-cell-contents row-tooltip text-center">'+
54 '<span ng-click="grid.appScope.toggleConfirmVuln(row.entity, row.entity.confirmed)" class="glyphicon glyphicon-filter cursor" uib-tooltip="{{grid.appScope.confirmedTooltip(row.entity.confirmed)}}" tooltip-placement="right" ng-class="{ disabled:row.entity.confirmed === false }"></span>'+
55 '</div>';
56
57 $scope.gridOptions = {
58 multiSelect: true,
59 enableSelectAll: true,
60 enableColumnMenus: false,
61 enableRowSelection: true,
62 enableRowHeaderSelection: false,
63 paginationPageSizes: [10, 50, 75, 100],
64 paginationPageSize: 10,
65 enableHorizontalScrollbar: 0,
66 treeRowHeaderAlwaysVisible: false,
67 enableGroupHeaderSelection: true,
68 rowHeight: 95
69 };
70 $scope.gridOptions.columnDefs = [];
71
72 $scope.showObjects = function(object, property, IdEvidence) {
73 var partial = "";
74 if(angular.isArray(object) === false) {
75 for(key in object) {
76 if(object.hasOwnProperty(key)) {
77 if(object[key] === true) {
78 partial += "<p class='pos-middle crop-text'>" + key + "</p>";
79 } else if(property === "evidence") {
80 partial += "<p class='pos-middle crop-text'><a href='http://127.0.0.1:5984/"+$scope.workspace+"/"+IdEvidence+"/"+$scope.encodeUrl(key)+"' target='_blank'>" + key + "</a></p>";
81 }
82 }
83 }
84 } else {
85 object.forEach(function(key) {
86 if(key === "") return;
87 if(property === "hostnames") {
88 partial += "<p><a href='//www.shodan.io/search?query=" + key + "' class=\"pos-middle crop-text\" uib-tooltip=\"Search in Shodan\" target=\"_blank\">" +
89 "<img src=\"../././reports/images/shodan.png\" height=\"15px\" width=\"15px\" style=\"margin-left:5px\"/></a>"+
90 "<a href=\""+ $scope.hash + "/search/"+ property +"=" + key + "\">" + key + "</a></p>";
91 } else if(property === "refs"){
92 partial += "<p class='pos-middle crop-text'><a href='" + $scope.processReference(key) + "' target=\"_blank\">" + key + "</a></p>";
93 } else {
94 partial += "<p class='pos-middle crop-text'>" + key + "</p>";
95 }
96 });
97 }
98 return partial;
99 };
100
101 $scope.gridOptions.onRegisterApi = function(gridApi){
102 //set gridApi on scope
103 $scope.gridApi = gridApi;
104 $scope.gridApi.selection.on.rowSelectionChanged( $scope, function ( rowChanged ) {
105 if ( typeof(rowChanged.treeLevel) !== 'undefined' && rowChanged.treeLevel > -1 ) {
106 // this is a group header
107 children = $scope.gridApi.treeBase.getRowChildren( rowChanged );
108 children.forEach( function ( child ) {
109 if ( rowChanged.isSelected ) {
110 $scope.gridApi.selection.selectRow( child.entity );
111 } else {
112 $scope.gridApi.selection.unSelectRow( child.entity );
113 }
114 });
115 }
116 });
117 $scope.gridApi.pagination.on.paginationChanged($scope, function (pageNumber, pageSize) {
118 $scope.gridApi.selection.clearSelectedRows();
119 });
120 };
48121
49122 // load all workspaces
50123 workspacesFact.list().then(function(wss) {
58131 $scope.search = $routeParams.search;
59132 $scope.searchParams = "";
60133 $scope.expression = {};
134 if($cookies.get('confirmed') === 'true') $scope.confirmed = true;
135 if($scope.confirmed === true) {
136 if($scope.search !== undefined) {
137 $scope.search = $scope.search.concat("&confirmed=true");
138 } else {
139 $scope.search = "confirmed=true";
140 }
141 }
61142 if($scope.search != "" && $scope.search != undefined && $scope.search.indexOf("=") > -1) {
62143 // search expression for filter
63144 $scope.expression = $scope.decodeSearch($scope.search);
64145 // search params for search field, which shouldn't be used for filtering
65146 $scope.searchParams = $scope.stringSearch($scope.expression);
66147 }
148 $scope.hash = window.location.hash;
149 if(window.location.hash.substring(1).indexOf('search') !== -1) {
150 $scope.hash = $scope.hash.slice(0, window.location.hash.indexOf('search') - 1);
151 }
67152
68153 // load all vulnerabilities
69154 vulnsManager.getVulns($scope.workspace).then(function(vulns) {
70 $scope.vulns = vulnsManager.vulns;
155 tmp_data = $filter('orderObjectBy')(vulnsManager.vulns, $scope.propertyGroupBy, true);
156 $scope.gridOptions.data = $filter('filter')(tmp_data, $scope.expression);
157
158 $scope.gridOptions.total = vulns.length;
159 if($scope.gridOptions.total > $scope.gridOptions.paginationPageSize && $scope.gridOptions.total > 100) {
160 $scope.gridOptions.paginationPageSizes.push($scope.gridOptions.total);
161 }
71162 });
72163
73164 // created object for columns cookie columns
74 if(typeof($cookies.SRcolumns) != 'undefined') {
165 if(typeof($cookies.get('SRcolumns')) != 'undefined'){
75166 var objectoSRColumns = {};
76 var arrayOfColumns = $cookies.SRcolumns.replace(/[{}"']/g, "").split(',');
167 var arrayOfColumns = $cookies.get('SRcolumns').replace(/[{}"']/g, "").split(',');
77168 arrayOfColumns.forEach(function(column){
78169 var columnFinished = column.split(':');
79170 if(columnFinished[1] == "true") objectoSRColumns[columnFinished[0]] = true; else objectoSRColumns[columnFinished[0]] = false;
82173 // set columns to show and hide by default
83174 $scope.columns = objectoSRColumns || {
84175 "date": true,
176 "name": true,
85177 "severity": true,
86178 "service": true,
87179 "target": true,
88 "name": true,
89180 "desc": true,
90181 "resolution": false,
91182 "data": true,
105196 "response": false,
106197 "web": false
107198 };
108
199 $scope.gridOptions.columnDefs.push({ name: ' ', width: '20', headerCellTemplate: "<i class=\"fa fa-check cursor\" ng-click=\"grid.appScope.selectAll()\" ng-style=\"{'opacity':(grid.appScope.selected === true) ? '1':'0.6'}\"></i>", pinnedLeft:true });
200 $scope.gridOptions.columnDefs.push({ name: ' ', width: '40', cellTemplate: filterRow });
201 $scope.gridOptions.columnDefs.push({ name: ' ', width: '40', cellTemplate: deleteRow });
202 $scope.gridOptions.columnDefs.push({ name: ' ', width: '30', cellTemplate: editRow });
203 var count = 0;
204 for(key in $scope.columns) {
205 if($scope.columns.hasOwnProperty(key) && $scope.columns[key] == true) {
206 count++;
207 _addColumn(key);
208 if(key === $scope.propertyGroupBy) {
209 $scope.gridOptions.columnDefs[count + 3].grouping = { groupPriority: 0 };
210 $scope.gridOptions.columnDefs[count + 3].sort = { priority: 0, direction: 'asc' }
211 }
212 }
213 }
109214 $scope.vulnWebSelected = false;
110215 };
111216
217 _addColumn = function(column) {
218
219 var myHeader = "<div ng-class=\"{ 'sortable': sortable }\">"+
220 "<div class=\"ui-grid-cell-contents\" col-index=\"renderIndex\" title=\"TOOLTIP\">{{ col.displayName CUSTOM_FILTERS }}"+
221 "<a href=\"\" ng-click=\"grid.appScope.toggleShow(col.displayName, true)\"><span style=\"color:#000;\" class=\"glyphicon glyphicon-remove\"></span></a>"+
222 "<span ui-grid-visible=\"col.sort.direction\" ng-class=\"{ 'ui-grid-icon-up-dir': col.sort.direction == asc, 'ui-grid-icon-down-dir': col.sort.direction == desc, 'ui-grid-icon-blank': !col.sort.direction }\">&nbsp;</span>"+
223 "</div>"+
224 "<div class=\"ui-grid-column-menu-button\" ng-if=\"grid.options.enableColumnMenus && !col.isRowHeader && col.colDef.enableColumnMenu !== false\" ng-click=\"toggleMenu($event)\" ng-class=\"{'ui-grid-column-menu-button-last-col': isLastCol}\">"+
225 "<i class=\"ui-grid-icon-angle-down\">&nbsp;</i>"+
226 "</div>"+
227 "<div ui-grid-filter></div>"
228 "</div>";
229
230 if(column === 'date') {
231 $scope.gridOptions.columnDefs.push({ 'name' : 'metadata.create_time', 'displayName' : column, type: 'date', cellFilter: 'date:"MM/dd/yyyy"', headerCellTemplate: myHeader, width: '90'
232 });
233 } else if(column === 'name') {
234 $scope.gridOptions.columnDefs.push({ 'name' : column,
235 'cellTemplate': '<div ng-if="row.entity._id != undefined"><div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents white-space" uib-tooltip="{{COL_FIELD}}"><a ng-href=' + $scope.hash + '/search/name={{row.entity.name}}>{{COL_FIELD CUSTOM_FILTERS}}</a></div></div><div ng-if=\"row.groupHeader && col.grouping.groupPriority !== undefined\" class="ui-grid-cell-contents white-space">{{COL_FIELD CUSTOM_FILTERS}}</div>',
236 headerCellTemplate: myHeader,
237 maxWidth: '230'
238 });
239 } else if(column === 'severity') {
240 $scope.gridOptions.columnDefs.push({ 'name' : column,
241 'cellTemplate': '<div ng-if="row.entity._id != undefined">'+
242 '<div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents text-center"><a href=\"#/status/ws/' + $scope.workspace + '/search/severity={{COL_FIELD}}\"><span class=\"label vuln fondo-{{COL_FIELD}}\">{{COL_FIELD | uppercase}}</span></a></div>'+
243 '</div>'+
244 '<div ng-if=\"row.groupHeader && col.grouping.groupPriority !== undefined\"><span class=\"label vuln fondo-{{COL_FIELD}}\">{{COL_FIELD | uppercase}}</span></div>',
245 headerCellTemplate: myHeader,
246 type: 'string',
247 width: '110',
248 sortingAlgorithm: compareSeverities
249 });
250 } else if(column === 'target') {
251 $scope.gridOptions.columnDefs.push({ 'name' : column, 'cellTemplate': "<div ng-if='row.entity._id != undefined' class='ui-grid-cell-contents row-tooltip'><a ng-href=" + $scope.hash + "/search/target={{row.entity.target}}>{{COL_FIELD CUSTOM_FILTERS}}</a>" +
252 "<a ng-href=\"//www.shodan.io/search?query={{row.entity.target}}\" uib-tooltip=\"Search in Shodan\" target=\"_blank\">" +
253 "<img ng-src=\"../././reports/images/shodan.png\" height=\"15px\" width=\"15px\" style='margin-left:5px'/>" +
254 "</a></div>"+
255 "<div ng-if=\"row.groupHeader && col.grouping.groupPriority !== undefined\">{{COL_FIELD CUSTOM_FILTERS}}</div>", headerCellTemplate: myHeader,
256 width: '115',
257 });
258 } else if(column === 'impact' || column === 'refs' || column === 'hostnames') {
259 $scope.gridOptions.columnDefs.push({ 'name' : column, 'displayName': column, 'cellTemplate': "<div class=\"ui-grid-cell-contents center\" ng-bind-html=\"grid.appScope.showObjects(COL_FIELD, col.name)\"></div><div ng-if=\"row.groupHeader && col.grouping.groupPriority !== undefined\">{{COL_FIELD CUSTOM_FILTERS}}</div>", headerCellTemplate: myHeader });
260 } else if(column === 'evidence') {
261 $scope.gridOptions.columnDefs.push({ 'name' : column, 'displayName': column, 'cellTemplate': "<div class=\"ui-grid-cell-contents center\" ng-bind-html=\"grid.appScope.showObjects(row.entity._attachments, col.name, row.entity._id)\"></div><div ng-if=\"row.groupHeader && col.grouping.groupPriority !== undefined\">{{COL_FIELD CUSTOM_FILTERS}}</div>", headerCellTemplate: myHeader });
262 } else if(column === 'service') {
263 $scope.gridOptions.columnDefs.push({ 'name' : column, 'displayName': column, 'cellTemplate': "<div class='ui-grid-cell-contents'><a href=" + $scope.hash + "/search/service={{grid.appScope.encodeUrl(row.entity.service)}}>{{COL_FIELD CUSTOM_FILTERS}}</a></div><div ng-if='row.groupHeader && col.grouping.groupPriority !== undefined'>{{COL_FIELD CUSTOM_FILTERS}}</div>", headerCellTemplate: myHeader, width: '110' });
264 } else if(column === 'web') {
265 $scope.gridOptions.columnDefs.push({ 'name' : column, 'displayName': column, width: '80',
266 'cellTemplate': "<div ng-if='row.entity._id != undefined' class=\"ui-grid-cell-contents center\">"+
267 "<span class=\"glyphicon glyphicon-ok\" ng-show=\"row.entity.type === 'VulnerabilityWeb'\"></span>"+
268 "<span class=\"glyphicon glyphicon-remove\" ng-show=\"row.entity.type !== 'VulnerabilityWeb'\"></span>"+
269 "</div>",
270 headerCellTemplate: myHeader
271 });
272 } else if(column === 'desc') {
273 $scope.gridOptions.columnDefs.push({ 'name' : column, headerCellTemplate: myHeader,
274 cellTemplate: '<div><div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents overflow-cell white-space" uib-tooltip=\"{{grid.appScope.ifTooltip(COL_FIELD)}}\">{{COL_FIELD CUSTOM_FILTERS}}</div></div>',
275 minWidth: '300',
276 maxWidth: '400'
277 });
278 } else if(column === 'data' || column === 'resolution' || column === 'request' || column === 'response') {
279 $scope.gridOptions.columnDefs.push({ 'name' : column, headerCellTemplate: myHeader,
280 cellTemplate: '<div><div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents white-space" uib-tooltip="{{COL_FIELD}}">{{COL_FIELD CUSTOM_FILTERS}}</div></div>'
281 });
282 } else {
283 $scope.gridOptions.columnDefs.push({ 'name' : column, headerCellTemplate: myHeader,
284 cellTemplate: '<div ng-if="row.entity._id != undefined"><div ng-if="!col.grouping || col.grouping.groupPriority === undefined || col.grouping.groupPriority === null || ( row.groupHeader && col.grouping.groupPriority === row.treeLevel )" class="ui-grid-cell-contents white-space" uib-tooltip="{{COL_FIELD}}"><a href=' + $scope.hash + '/search/{{col.name}}={{grid.appScope.encodeUrl(row.entity[col.name])}}>{{COL_FIELD CUSTOM_FILTERS}}</a></div></div><div ng-if=\"row.groupHeader && col.grouping.groupPriority !== undefined\" class="ui-grid-cell-contents white-space">{{COL_FIELD CUSTOM_FILTERS}}</div>', });
285 }
286 };
287
288 $scope.ifTooltip = function(text) {
289 if(text !== undefined && text.length > 450) {
290 return text;
291 }
292 };
293
294 $scope.confirmedTooltip = function(isConfirmed) {
295 var res = "";
296 if(isConfirmed === true) {
297 res = "Change to false positive";
298 } else {
299 res = "Confirm";
300 }
301 return res;
302 };
303
304 $scope.selectAll = function() {
305 if($scope.selected === false) {
306 for(var i = 0; i <= $scope.gridOptions.paginationPageSize; i++) {
307 $scope.gridApi.selection.selectRowByVisibleIndex(i);
308 }
309 $scope.selected = true;
310 } else {
311 $scope.gridApi.selection.clearSelectedRows();
312 var allVisibleRows = $scope.gridApi.core.getVisibleRows($scope.gridApi);
313 allVisibleRows.forEach(function(row) {
314 if(row.groupHeader === true && row.isSelected === true) {
315 row.isSelected = false;
316 }
317 });
318 $scope.selected = false;
319 }
320 };
321
112322 $scope.processReference = function(text) {
113323 var url = 'http://google.com/',
114324 url_pattern = new RegExp('^(http|https):\\/\\/?');
137347 url += 'search?q=' + text;
138348 }
139349
140 $window.open(url, '_blank');
141 };
142
143 $scope.selectedVulns = function() {
144 selected = [];
145 var tmp_vulns = $filter('orderObjectBy')($scope.vulns, $scope.sortField, $scope.reverse);
146 tmp_vulns = $filter('filter')(tmp_vulns, $scope.expression);
147 tmp_vulns = tmp_vulns.splice($scope.pageSize * $scope.currentPage, $scope.pageSize);
148 tmp_vulns.forEach(function(vuln) {
149 if (vuln.selected_statusreport_controller) {
150 selected.push(vuln);
151 }
152 });
153 return selected;
154 }
350 return url;
351 };
352
353 $scope.groupBy = function(property) {
354 var url = "/status/ws/" + $routeParams.wsId + "/groupby/" + property;
355
356 $location.path(url);
357 };
358
359 $scope.clearGroupBy = function() {
360 var url = "/status/ws/" + $routeParams.wsId;
361
362 $location.path(url);
363 };
364
365 $scope.getCurrentSelection = function() {
366 return $scope.gridApi.selection.getSelectedRows();
367 };
368
369 $scope.encodeUrl = function(text) {
370 return encodeURIComponent(encodeURIComponent(text));
371 };
155372
156373 $scope.csv = function() {
157 tmp_vulns = $filter('filter')($scope.vulns, $scope.expression);
374 tmp_vulns = $filter('filter')($scope.gridOptions.data, $scope.expression);
158375 return csvService.generator($scope.columns, tmp_vulns, $scope.workspace);
159376 };
160377
378 $scope.toggleFilter = function(expression) {
379 if(expression["confirmed"] === undefined) {
380 expression["confirmed"] = true;
381 $scope.expression = expression;
382 $cookies.put('confirmed', $scope.expression.confirmed);
383 loadVulns();
384 $scope.confirmed = true;
385 } else {
386 $scope.expression = {};
387 for(key in expression) {
388 if(expression.hasOwnProperty(key)) {
389 if(key !== "confirmed") {
390 $scope.expression[key] = expression[key];
391 }
392 }
393 }
394 $cookies.put('confirmed', $scope.expression.confirmed);
395 loadVulns();
396 $scope.confirmed = false;
397 }
398 };
399
161400 showMessage = function(msg) {
162 var modal = $modal.open({
401 var modal = $uibModal.open({
163402 templateUrl: 'scripts/commons/partials/modalKO.html',
164403 controller: 'commonsModalKoCtrl',
165404 resolve: {
174413 $scope.remove = function(aVulns) {
175414 aVulns.forEach(function(vuln) {
176415 vulnsManager.deleteVuln(vuln)
177 .then(function() {})
416 .then(function() {
417 loadVulns();
418 })
178419 .catch(function(errorMsg) {
179420 // TODO: show errors somehow
180421 console.log("Error deleting vuln " + vuln._id + ": " + errorMsg);
185426
186427 // action triggered from DELETE button
187428 $scope.delete = function() {
188 _delete($scope.selectedVulns());
429 _delete($scope.getCurrentSelection());
189430 };
190431 // delete only one vuln
191432 $scope.deleteVuln = function(vuln) {
194435
195436 _delete = function(vulns) {
196437 if(vulns.length > 0) {
197 var modal = $modal.open({
438 var modal = $uibModal.open({
198439 templateUrl: 'scripts/commons/partials/modalDelete.html',
199440 controller: 'commonsModalDelete',
200441 size: 'lg',
220461 }
221462 };
222463
464 $scope.toggleConfirmVuln = function(vuln, confirm) {
465 _toggleConfirm([vuln], confirm);
466 };
467
468 _toggleConfirm = function(vulns, confirm) {
469 var toggleConfirm = {'confirmed': !confirm},
470 deferred = $q.defer(),
471 promises = [];
472 vulns.forEach(function(vuln) {
473 promises.push(vulnsManager.updateVuln(vuln, toggleConfirm));
474 });
475 $q.all(promises).then(function(res) {
476 if(confirm === true) {
477 loadVulns();
478 }
479 }, function(errorMsg){
480 showMessage("Error updating vuln " + vuln.name + " (" + vuln._id + "): " + errorMsg);
481 });
482 };
483
223484 // action triggered from EDIT button
224485 $scope.edit = function() {
225 _edit($scope.selectedVulns());
486 _edit($scope.getCurrentSelection());
226487 };
227488
228489 $scope.editVuln = function(vuln) {
231492
232493 _edit = function(vulns) {
233494 if (vulns.length == 1) {
234 var modal = $modal.open({
495 var modal = $uibModal.open({
235496 templateUrl: 'scripts/statusReport/partials/modalEdit.html',
236497 controller: 'modalEditCtrl as modal',
237498 size: 'lg',
268529 return opts.options;
269530 }
270531 };
271 var modal = $modal.open({
532 var modal = $uibModal.open({
272533 templateUrl: partial,
273534 controller: controller,
274535 size: 'lg',
275536 resolve: resolve
276537 });
277538 modal.result.then(function(data) {
278 $scope.selectedVulns().forEach(function(vuln) {
539 $scope.getCurrentSelection().forEach(function(vuln) {
279540 obj = {};
280541 obj[property] = data;
281542
386647 property);
387648 };
388649
650 $scope.editConfirm = function() {
651 editProperty(
652 'scripts/commons/partials/editOptions.html',
653 'commonsModalEditOptions',
654 'Confirm/Change to false positive:',
655 'confirmed',
656 {
657 options: ['Confirm', 'Set to false positive'],
658 callback: function(vuln, data) {
659 var property;
660 if(data === 'Confirm') {
661 property = {'confirmed': true};
662 } else {
663 property = {'confirmed': false};
664 }
665 return property;
666 }
667 }
668 );
669
670 };
671
389672 $scope.editCWE = function() {
390 var modal = $modal.open({
673 var modal = $uibModal.open({
391674 templateUrl: 'scripts/commons/partials/editCWE.html',
392675 controller: 'commonsModalEditCWE',
393676 size: 'lg',
418701
419702 $scope.insert = function(vuln) {
420703 vulnsManager.createVuln($scope.workspace, vuln).then(function() {
704 loadVulns();
421705 }, function(message) {
422706 var msg = "The vulnerability couldn't be created";
423707 if(message == "409") {
435719 */
436720 };
437721
722 loadVulns = function(property) {
723 // load all vulnerabilities
724 if (!property) property = "name";
725 tmp_data = $filter('orderObjectBy')(vulnsManager.vulns, property, true);
726 $scope.gridOptions.data = $filter('filter')(tmp_data, $scope.expression);
727 };
728
438729 $scope.new = function() {
439 var modal = $modal.open({
730 var modal = $uibModal.open({
440731 templateUrl: 'scripts/statusReport/partials/modalNew.html',
441732 controller: 'modalNewVulnCtrl as modal',
442733 size: 'lg',
455746 });
456747 };
457748
458 $scope.checkAll = function() {
459 if(!$scope.selectall) {
460 $scope.selectall = true;
461 } else {
462 $scope.selectall = false;
463 }
464
465 var tmp_vulns = $filter('orderObjectBy')($scope.vulns, $scope.sortField, $scope.reverse);
466 tmp_vulns = $filter('filter')(tmp_vulns, $scope.expression);
467 tmp_vulns = tmp_vulns.splice($scope.pageSize * $scope.currentPage, $scope.pageSize);
468 tmp_vulns.forEach(function(v,k) {
469 v.selected_statusreport_controller = $scope.selectall;
470 });
471
472 };
473
474 $scope.go = function() {
475 $scope.pageSize = $scope.newPageSize;
476 $cookies.pageSize = $scope.pageSize;
477 $scope.currentPage = 0;
478 if($scope.newCurrentPage <= parseInt($scope.vulns.length/$scope.pageSize)
479 && $scope.newCurrentPage > -1 && !isNaN(parseInt($scope.newCurrentPage))) {
480 $scope.currentPage = $scope.newCurrentPage;
481 }
482 };
483
484749 // encodes search string in order to send it through URL
485750 $scope.encodeSearch = function(search) {
486751 var i = -1,
545810 if(prop == "$") {
546811 search += obj[prop];
547812 } else {
548 search += prop + ":" + obj[prop];
813 if(prop !== "confirmed"){
814 search += prop + ":" + obj[prop];
815 }
549816 }
550817 }
551818 }
555822
556823 // changes the URL according to search params
557824 $scope.searchFor = function(search, params) {
558 var url = "/status/ws/" + $routeParams.wsId;
825 if(window.location.hash.substring(1).indexOf('groupby') === -1) {
826 var url = "/status/ws/" + $routeParams.wsId;
827 } else {
828 var url = "/status/ws/" + $routeParams.wsId + "/groupby/" + $routeParams.groupbyId;
829 }
559830
560831 if(search && params != "" && params != undefined) {
561832 url += "/search/" + $scope.encodeSearch(params);
566837
567838 // toggles column show property
568839 $scope.toggleShow = function(column, show) {
840 column = column.toLowerCase();
569841 $scope.columns[column] = !show;
570 $cookies.SRcolumns = JSON.stringify($scope.columns);
571 };
572
573 // toggles sort field and order
574 $scope.toggleSort = function(field) {
575 $scope.toggleSortField(field);
576 $scope.toggleReverse();
577 };
578
579 // toggles column sort field
580 $scope.toggleSortField = function(field) {
581 $scope.sortField = field;
582 };
583
584 // toggle column sort order
585 $scope.toggleReverse = function() {
586 $scope.reverse = !$scope.reverse;
587 };
588
589 $scope.selectionChange = function() {
590 $scope.vulnWebSelected = $scope.selectedVulns().some(function(v) {
591 return v.type === "VulnerabilityWeb"
592 });
842 for (i = 0;i < $scope.gridOptions.columnDefs.length; i++) {
843 if($scope.gridOptions.columnDefs[i].name === column) {
844 $scope.gridOptions.columnDefs.splice(i, 1);
845 } else {
846 if(show === false) {
847 _addColumn(column);
848 break;
849 }
850 }
851 }
852 $cookies.put('SRcolumns', JSON.stringify($scope.columns));
853 };
854
855 var compareSeverities = function(a, b) {
856 if(a !== 'undefined' || b !== 'undefined') {
857 var res = 1;
858 if($scope.severities.indexOf(a) === $scope.severities.indexOf(b)) { return 0; }
859 if($scope.severities.indexOf(a) > $scope.severities.indexOf(b)) {
860 res = -1;
861 }
862 return res;
863 }
593864 };
594865
595866 init();
1919 <div class="form-group">
2020 <div class="col-md-12">
2121 <h5>CWE</h5>
22 <input type="text" ng-model="modal.cwe_selected" class="form-control input-sm" placeholder="Search for CWE" typeahead="cwe as cwe.name for cwe in modal.cweList | filter:{name: $viewValue} | limitTo:10" typeahead-on-select="modal.populate($item, $model, $label)">
22 <input type="text" ng-model="modal.cwe_selected" class="form-control input-sm" placeholder="Search for CWE" uib-typeahead="cwe as cwe.name for cwe in modal.cweList | filter:{name: $viewValue} | limitTo:10" typeahead-on-select="modal.populate($item, $model, $label)">
2323 </div>
2424 </div>
2525 <div class="form-group">
26 <div class="col-md-6 severities">
26 <div class="col-md-4 severities">
2727 <h5>Severity</h5>
2828 <button type="button" class="btn btn-default dropdown-toggle color-{{modal.data.severity}}" name="severity" data-toggle="dropdown" title="Change severity" ng-class="{'button-error': modal.data.severity === undefined}">
2929 {{modal.data.severity || 'Edit severity'}} <span class="caret" style="color:#000"></span>
3232 <li ng-repeat="s in modal.severities"><a href="" class="ws color-{{s}}" ng-click="modal.data.severity=s">{{s}}</a></li>
3333 </ul><!-- WS navigation -->
3434 </div>
35 <div class="col-md-6">
35 <div class="col-md-4">
3636 <h5>Ease of Resolution</h5>
3737 <select class="form-control" ng-model="modal.data.easeofresolution" ng-options="e as e for e in modal.easeofresolution">
3838 <option value=""></option>
3939 </select>
40 </div>
41 <div class="col-md-4">
42 <h5>Confirmed</h5>
43 <input type="checkbox" id="confirmed" ng-model="modal.data.confirmed" />
44 <span class="normal-size">Confirmed</span>
4045 </div>
4146 </div><!-- .form-group -->
4247 <div class="form-group" ng-class="{'has-error': formEdit.name.$invalid }">
1616 <div class="form-group input-accordion">
1717 <input type="text" ng-model="modal.target_filter" class="form-control input-sm" placeholder="Search" ng-change="modal.currentPage = 0">
1818 </div>
19 <accordion close-others="true">
20 <accordion-group is-open="isopen" ng-repeat="host in modal.targets_filtered = (modal.targets | filter:modal.target_filter) | startFrom:modal.currentPage*modal.pageSize | limitTo:modal.pageSize">
21 <accordion-heading>
19 <uib-accordion close-others="true">
20 <uib-accordion-group is-open="isopen" ng-repeat="host in modal.targets_filtered = (modal.targets | filter:modal.target_filter) | startFrom:modal.currentPage*modal.pageSize | limitTo:modal.pageSize">
21 <uib-accordion-heading>
2222 <a ng-click="modal.setTarget(host)" ng-class="{'multi-selected': host.selected_modalNewCtrl == true}">{{host.name}} ({{host.hostnames[0]}})</a>
2323 <i class="pull-right glyphicon"
2424 ng-class="{'glyphicon glyphicon-minus-sign': isopen, 'glyphicon glyphicon-plus-sign': !isopen}"></i>
25 </accordion-heading>
25 </uib-accordion-heading>
2626 <div class="panel-body" ng-repeat="service in host.services">
27 <a ng-model="service" ng-click="modal.setTarget(service)" ng-class="{'multi-selected': service.selected_modalNewCtrl == true}">{{service.name}}</a>
28 </div>
29 </accordion-group>
30 </accordion>
27 <a ng-model="service" ng-click="modal.setTarget(service)" ng-class="{'multi-selected': service.selected_modalNewCtrl == true}">{{service.name}} {{service.ports}}</a>
28 </div>
29 </uib-accordion-group>
30 </uib-accordion>
3131 <div class="showPagination" ng-show="modal.targets_filtered.length > modal.pageSize">
3232 <div class="form-group">
3333 <ul class="pagination">
6161 <div class="form-group">
6262 <div class="col-md-12">
6363 <h5>CWE</h5>
64 <input type="text" ng-model="modal.cwe_selected" class="form-control input-sm" placeholder="Search for CWE" typeahead="cwe as cwe.name for cwe in modal.cweList | filter:{name: $viewValue} | limitTo:10" typeahead-on-select="modal.populate($item, $model, $label)">
64 <input type="text" ng-model="modal.cwe_selected" class="form-control input-sm" placeholder="Search for CWE" uib-typeahead="cwe as cwe.name for cwe in modal.cweList | filter:{name: $viewValue} | limitTo:10" typeahead-on-select="modal.populate($item, $model, $label)">
6565 </div>
6666 </div>
6767 <div class="form-group">
55 <div class="right-main">
66 <div id="reports-main" class="fila clearfix">
77 <div class="ws-label">
8 <h2><span id="ws-name" title="Current workspace">Status report for {{ workspace }} ({{vulns.length}} vulns)</span></h2><!-- WS name -->
8 <h2><span id="ws-name" title="Current workspace">Status report for {{ workspace }} ({{ confirmed === false ? 'all vulns' : 'confirmed' }}) ({{gridOptions.data.length}} vulns)</span></h2><!-- WS name -->
99 </div><!-- .ws-label -->
1010 <div id="ws-control" class="btn-group btn-small-margin">
1111 <button file-exporter="csv()" type="button" class="btn btn-success" title="Download CSV for current workspace">
1313 </button>
1414 <button id="refresh" type="button" class="btn btn-danger" title="Refresh current workspace" ng-click="location.reload()">
1515 <span class="glyphicon glyphicon-refresh"></span>
16 </button>
17 <button type="button" class="btn btn-danger" title="{{ confirmed === true ? 'All vulns' : 'Confirmed vulns' }}" ng-click="toggleFilter(expression)">
18 <span class="glyphicon glyphicon-filter" ng-style="{ 'opacity': (confirmed === true) ? '1' : '0.7' }"></span>
1619 </button>
1720 <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" title="Change current workspace">
1821 Change workspace <span class="caret"></span>
2831 Delete
2932 </button>
3033 <div id="merge" class="btn-group btn-small-margin">
31 <button type="button" class="btn btn-default" title="Edit selected vulns" ng-click="edit()" ng-disabled="selectedVulns().length != 1">
34 <button type="button" class="btn btn-default" title="Edit selected vulns" ng-click="edit()" ng-disabled="getCurrentSelection().length != 1">
3235 <span class="glyphicon glyphicon-pencil"></span>
3336 Edit
3437 </button>
35 <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Actions" ng-hide="selectedVulns().length < 2">
38 <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Actions" ng-hide="getCurrentSelection().length < 2">
3639 <span class="caret"></span>
3740 </button>
3841 <ul class="dropdown-menu dropdown-menu-right" role="menu">
4447 <li><a class="ws" ng-click="editEaseofresolution()">Edit ease of resolution</a></li>
4548 <li><a class="ws" ng-click="editReferences()">Add references</a></li>
4649 <li><a class="ws" ng-click="editImpact()">Edit impact</a></li>
50 <li><a class="ws" ng-click="editConfirm()">Confirm/Change to false positive</a></li>
4751 <li ng-show="vulnWebSelected" role="separator" class="divider"></li>
4852 <li ng-show="vulnWebSelected"><a class="ws" ng-click="editString('method')">Edit method</a></li>
4953 <li ng-show="vulnWebSelected"><a class="ws" ng-click="editString('pname', 'param name')">Edit param name</a></li>
5660 <li role="separator" class="divider"></li>
5761 <li><a class="ws" ng-click="editCWE()">From CWE</a></li>
5862 </ul>
59 </div>
63 </div>
64 <div id="group-by" class="btn-group btn-small-margin">
65 <button type="button" class="btn btn-default" title="Group by" ng-click="clearGroupBy()">
66 {{ !propertyGroupBy ? "Group By" : "Clear"}}
67 </button>
68 <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Group By">
69 <span class="caret"></span>
70 </button>
71 <ul class="dropdown-menu dropdown-menu-right" role="menu">
72 <li ng-repeat="column in colProperties"><a class="ws" ng-click="groupBy(column)">{{column}}</a></li>
73 </ul>
74 </div>
6075 <button id="new" type="button" class="btn btn-success" title="New Vulns" ng-click="new()">
6176 <span class="glyphicon glyphicon-plus-sign"></span>
6277 New
6883 <form role="form" ng-submit="searchFor(true, searchParams)">
6984 <div class="form-group">
7085 <div class="input-group input-group-sm">
71 <span class="input-group-addon glyphicon-btn glyphicon glyphicon-remove" ng-click="searchFor(false, '')" ng-if="search"></span>
86 <span class="input-group-addon glyphicon-btn glyphicon glyphicon-remove" ng-click="searchFor(false, '')" ng-if="search && search != 'confirmed=true'"></span>
7287 <input type="text" class="form-control" id="filter-by"
7388 placeholder="enter keywords" ng-change="currentPage = 0" ng-model="searchParams" />
7489 <span class="input-group-addon glyphicon-btn" ng-click="searchFor(true, searchParams)">
75 <i class="fa fa-search" ng-if="vulns"></i>
76 <i class="fa fa-refresh fa-spin" ng-if="vulns.length == 0"></i>
90 <i class="fa fa-search" ng-if="gridOptions.data.length > 0"></i>
91 <i class="fa fa-refresh fa-spin" ng-if="gridOptions.data.length == 0"></i>
7792 </span>
7893 </div>
7994 </div>
91106 </div>
92107 </div>
93108
94 <table class="csv-export status-report table table-responsive">
95 <thead>
96 <tr>
97 <th><input type="checkbox" ng-model="selectall" ng-click="checkAll()"/></th>
98 <th><a href=""></a></th>
99 <th><a href=""></a></th>
100 <th ng-if="columns.date">
101 <a href="" ng-click="toggleSort('metadata.create_time')">Date</a>
102 <a href="" ng-click="toggleShow('date', true)"><span class="glyphicon glyphicon-remove"></span></a>
103 </th>
104 <th ng-if="columns.target">
105 <a href="" ng-click="toggleSort('target')">Target</a>
106 <a href="" ng-click="toggleShow('target', true)"><span class="glyphicon glyphicon-remove"></span></a>
107 </th>
108 <th ng-if="columns.service">
109 <a href="" ng-click="toggleSort('service')">Service</a>
110 <a href="" ng-click="toggleShow('service', true)"><span class="glyphicon glyphicon-remove"></span></a>
111 </th>
112 <th ng-if="columns.status">
113 <a href="" ng-click="toggleSort('status')">Status</a>
114 <a href="" ng-click="toggleShow('status', true)"><span class="glyphicon glyphicon-remove"></span></a>
115 </th>
116 <th ng-if="columns.severity">
117 <a href="" ng-click="toggleSort('severity')">Severity</a>
118 <a href="" ng-click="toggleShow('severity', true)"><span class="glyphicon glyphicon-remove"></span></a>
119 </th>
120 <th ng-if="columns.name">
121 <a href="" ng-click="toggleSort('name')">Name</a>
122 <a href="" ng-click="toggleShow('name', true)"><span class="glyphicon glyphicon-remove"></span></a>
123 </th>
124 <th ng-if="columns.desc">
125 <a href="" ng-click="toggleSort('desc')">Desc</a>
126 <a href="" ng-click="toggleShow('desc', true)"><span class="glyphicon glyphicon-remove"></span></a>
127 </th>
128 <th ng-if="columns.data">
129 <a href="" ng-click="toggleSort('data')">Data</a>
130 <a href="" ng-click="toggleShow('data', true)"><span class="glyphicon glyphicon-remove"></span></a>
131 </th>
132 <th ng-if="columns.method">
133 <a href="" ng-click="toggleSort('method')">Method</a>
134 <a href="" ng-click="toggleShow('method', true)"><span class="glyphicon glyphicon-remove"></span></a>
135 </th>
136 <th ng-if="columns.path">
137 <a href="" ng-click="toggleSort('path')">Path</a>
138 <a href="" ng-click="toggleShow('path', true)"><span class="glyphicon glyphicon-remove"></span></a>
139 </th>
140 <th ng-if="columns.pname">
141 <a href="" ng-click="toggleSort('pname')">Param Name</a>
142 <a href="" ng-click="toggleShow('pname', true)"><span class="glyphicon glyphicon-remove"></span></a>
143 </th>
144 <th ng-if="columns.params">
145 <a href="" ng-click="toggleSort('params')">Params</a>
146 <a href="" ng-click="toggleShow('params', true)"><span class="glyphicon glyphicon-remove"></span></a>
147 </th>
148 <th ng-if="columns.query">
149 <a href="" ng-click="toggleSort('query')">Query</a>
150 <a href="" ng-click="toggleShow('query', true)"><span class="glyphicon glyphicon-remove"></span></a>
151 </th>
152 <th ng-if="columns.request">
153 <a href="" ng-click="toggleSort('request')">Request</a>
154 <a href="" ng-click="toggleShow('request', true)"><span class="glyphicon glyphicon-remove"></span></a>
155 </th>
156 <th ng-if="columns.response">
157 <a href="" ng-click="toggleSort('response')">Response</a>
158 <a href="" ng-click="toggleShow('response', true)"><span class="glyphicon glyphicon-remove"></span></a>
159 </th>
160 <th ng-if="columns.resolution">
161 <a href="" ng-click="toggleSort('resolution')">Resolution</a>
162 <a href="" ng-click="toggleShow('resolution', true)"><span class="glyphicon glyphicon-remove"></span></a>
163 </th>
164 <th ng-if="columns.web">
165 <a href="" ng-click="toggleSort('type')">Web</a>
166 <a href="" ng-click="toggleShow('web', true)"><span class="glyphicon glyphicon-remove"></span></a>
167 </th>
168 <th ng-if="columns.website">
169 <a href="" ng-click="toggleSort('website')">Website</a>
170 <a href="" ng-click="toggleShow('website', true)"><span class="glyphicon glyphicon-remove"></span></a>
171 </th>
172 <th ng-if="columns.refs">
173 <a href="" ng-click="toggleSort('refs')">References</a>
174 <a href="" ng-click="toggleShow('refs', true)"><span class="glyphicon glyphicon-remove"></span></a>
175 </th>
176 <th ng-if="columns.evidence">
177 <a href="" ng-click="toggleSort('attachments')">Evidence</a>
178 <a href="" ng-click="toggleShow('evidence', true)"><span class="glyphicon glyphicon-remove"></span></a>
179 </th>
180 <th ng-if="columns.impact">
181 <a href="" ng-click="toggleSort('impact')">Impact</a>
182 <a href="" ng-click="toggleShow('impact', true)"><span class="glyphicon glyphicon-remove"></span></a>
183 </th>
184 <th ng-if="columns.easeofresolution">
185 <a href="" ng-click="toggleSort('easeofresolution')">Ease of Resolution</a>
186 <a href="" ng-click="toggleShow('easeofresolution', true)"><span class="glyphicon glyphicon-remove"></span></a>
187 </th>
188 <th ng-if="columns.hostnames">
189 <a href="" ng-click="toggleSort('hostnames')">Hostnames</a>
190 <a href="" ng-click="toggleShow('hostnames', true)"><span class="glyphicon glyphicon-remove"></span></a>
191 </th>
192 </tr>
193 </thead>
194 <tbody>
195 <tr ng-repeat="v in filtered = (vulns | filter:expression) | orderObjectBy:sortField:reverse | startFrom:currentPage*pageSize | limitTo:pageSize"
196 selection-model selection-model-type="checkbox"
197 selection-model-mode="multiple-additive"
198 selection-model-selected-attribute="selected_statusreport_controller"
199 selection-model-selected-class="multi-selected"
200 selection-model-on-change="selectionChange()">
201 <td class="text-center"><input type="checkbox" name="{{v.id}}"/></td>
202 <td class="text-center"><span ng-click="editVuln(v)" class="glyphicon glyphicon-pencil cursor" tooltip="Edit"></span></td>
203 <td class="text-center"><span ng-click="deleteVuln(v)" class="glyphicon glyphicon-trash cursor" tooltip="Delete"></span></td>
204 <td ng-if="columns.date">{{v.metadata.create_time * 1000 | date:'MM/dd/yyyy'}}</td>
205 <td ng-if="columns.target">
206 <a ng-href="#/status/ws/{{workspace}}/search/target={{v.target}}">{{v.target}}</a>
207 <a ng-href="//www.shodan.io/search?query={{v.target}}" tooltip="Search in Shodan" target="_blank">
208 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
209 </a>
210 </td>
211 <td ng-if="columns.service">
212 <a ng-href="#/status/ws/{{workspace}}/search/service={{v.service | encodeURIComponent | encodeURIComponent}}">{{v.service}}</a>
213 </td>
214 <td ng-if="columns.status">Vulnerable</td>
215 <td ng-if="columns.severity"><a href="#/status/ws/{{workspace}}/search/severity={{v.severity}}"><span class="label vuln fondo-{{v.severity}}">{{v.severity}}</span></a></td>
216 <td ng-if="columns.name"><a href="#/status/ws/{{workspace}}/search/name={{v.name | encodeURIComponent | encodeURIComponent}}">{{v.name}}</a></td>
217 <td ng-if="columns.desc" text-collapse text-collapse-max-length="150" text-collapse-text="{{v.desc}}"></td>
218 <td ng-if="columns.data" text-collapse text-collapse-max-length="150" text-collapse-text="{{v.data}}"></td>
219 <td ng-if="columns.method"><a href="#/status/ws/{{workspace}}/search/method={{v.method}}">{{v.method}}</a></td>
220 <td ng-if="columns.path"><a href="#/status/ws/{{workspace}}/search/path={{v.path}}">{{v.path}}</a></td>
221 <td ng-if="columns.pname"><a href="#/status/ws/{{workspace}}/search/pname={{v.pname}}">{{v.pname}}</a></td>
222 <td ng-if="columns.params">{{v.params}}</td>
223 <td ng-if="columns.query"><a href="#/status/ws/{{workspace}}/search/query={{v.query}}">{{v.query}}</a></td>
224 <td ng-if="columns.request" text-collapse text-collapse-max-length="100" text-collapse-text="{{v.request}}"></td>
225 <td ng-if="columns.response" text-collapse text-collapse-max-length="100" text-collapse-text="{{v.response}}"></td>
226 <td ng-if="columns.resolution">{{v.resolution}}</td>
227 <td ng-if="columns.web">
228 <span class="glyphicon glyphicon-ok" ng-show="v.type === 'VulnerabilityWeb'"></span>
229 <span class="glyphicon glyphicon-remove" ng-show="v.type !== 'VulnerabilityWeb'"></span>
230 </td>
231 <td ng-if="columns.website"><a href="#/status/ws/{{workspace}}/search/website={{v.website}}">{{v.website}}</a></td>
232 <td ng-if="columns.refs">
233 <p ng-repeat="refs in v.refs track by $index"><a ng-click="processReference(refs)">{{refs}}</a></p>
234 </td>
235 <td ng-if="columns.evidence">
236 <div ng-repeat="(name, file) in v._attachments track by $index">
237 <a ng-href="{{baseurl + workspace}}/{{v._id}}/{{name | encodeURIComponent}}" target="_blank">{{name | decodeURIComponent}}</a>
238 </div>
239 </td>
240 <td ng-if="columns.impact">
241 <div ng-repeat="(impact, rating) in v.impact">
242 <p ng-if="rating">{{impact}}</p>
243 </div>
244 </td>
245 <td ng-if="columns.easeofresolution"><a href="#/status/ws/{{workspace}}/search/easeofresolution={{v.easeofresolution}}">{{v.easeofresolution}}</a></td>
246 <td ng-if="columns.hostnames">
247 <p ng-repeat="hostname in v.hostnames track by $index">
248 <a href="#/status/ws/{{workspace}}/search/hostnames={{hostname}}">{{hostname}}</a>
249 <a href="//www.shodan.io/search?query={{hostname}}" tooltip="Search in Shodan" target="_blank">
250 <img ng-src="../././reports/images/shodan.png" height="15px" width="15px" />
251 </a>
252 </p>
253 </td>
254 </tr>
255 </tbody>
256 </table><!-- #hosts -->
257 <div class="showPagination">
258 <div class="form-group">
259 <ul class="pagination">
260 <li><a ng-hide="currentPage <= 0" ng-click="currentPage = currentPage - 1"><span aria-hidden="true">&laquo;</span><span class="sr-only">Previous</span></a></li>
261 <li><a>{{currentPage}}/{{ ((filtered.length / pageSize) | integer)}}</a></li>
262 <li><a ng-hide="currentPage >= ((filtered.length / pageSize) | integer)" ng-click="currentPage = currentPage + 1"><span aria-hidden="true">&raquo;</span><span class="sr-only">Next</span></a></li>
263 </ul>
264 <form name="goToPage" id="goToPageStatus">
265 <div class="col-md-2">
266 <input type="number" min="0" max="{{ (filtered.length / pageSize) | integer }}" class="form-control" ng-model="newCurrentPage" placeholder="Go to page"/>
267 </div>
268 <button class="btn btn-default" ng-click="go()">GO</button>
269 <input type="number" min="0" class="form-control vuln_per_page" ng-model=newPageSize placeholder="Number page" />
270 </form>
271 </div>
272 </div>
109 <div ui-grid="gridOptions" ui-grid-grouping ui-grid-selection ui-grid-pagination ui-grid-pinning class="grid"></div>
110
273111 <div id="counter">
274112 <span>Total</span>
275 <span class="counterNum">{{vulns.length}}</span>
113 <span class="counterNum">{{gridOptions.total}}</span>
276114 <div ng-if="search">
277115 <span>Viewing</span>
278 <span class="counterNum">{{filtered.length}}</span>
116 <span class="counterNum">{{gridOptions.data.length}}</span>
279117 </div>
280118 <p style="margin:0px" ng-if="!search"></p>
281119 <span>Selected</span>
282 <span class="counterNum">{{selectedVulns().length}}</span>
120 <span class="counterNum">{{getCurrentSelection().length}}</span>
283121 </div>
284122 </div><!-- .reports -->
285123 </div><!-- #reports-main --></div><!-- .right-main -->
140140 }
141141 if(services.hasOwnProperty(vuln.parent)) vuln.service = services[vuln.parent];
142142 });
143 deferred.resolve(self.vulns);
144 });
145 })
146 .error(function() {
147 deferred.reject("Unable to retrieve vulnerabilities from Couch");
148 });
149
150 return deferred.promise;
151 };
152
153 vulnsManager.getConfirmedVulns = function(ws) {
154 var deferred = $q.defer(),
155 self = this;
156
157 $http.get(BASEURL + ws + '/_design/vulns/_view/all')
158 .success(function(data) {
159 self.vulns.splice(0, self.vulns.length);
160 self.vulns_indexes = {};
161 for(var i = 0; i < data.rows.length; i++) {
162 var vulnData = data.rows[i].value;
163 if(vulnData.confirmed === true) {
164 try {
165 if(vulnData.type == "Vulnerability") {
166 var vuln = new Vuln(ws, vulnData);
167 } else {
168 var vuln = new WebVuln(ws, vulnData);
169 }
170 self.vulns_indexes[vuln._id] = self.vulns.length;
171 self.vulns.push(vuln);
172 } catch(e) {
173 console.log(e.stack);
174 }
175 }
176 }
177
178 var parents = [hostsManager.getHosts(ws), hostsManager.getAllInterfaces(ws)];
179
180 $q.all(parents)
181 .then(function(ps) {
182 var hosts = self._loadHosts(ps[0], ps[1]);
183
184 self.vulns.forEach(function(vuln) {
185 var pid = vuln.parent.split(".")[0];
186 if (hosts.hasOwnProperty(pid)) {
187 vuln.target = hosts[pid]["target"];
188 vuln.hostnames = hosts[pid]["hostnames"];
189 }
190 });
143191 });
144192
145193 deferred.resolve(self.vulns);
156204 self = this;
157205 vuln.update(data).then(function(){
158206 self.vulns[self.vulns_indexes[vuln._id]] = vuln;
207 deferred.resolve(vuln);
159208 }, function(err){
160209 deferred.reject(err);
161210 });
162 return deferred.promise
211 return deferred.promise;
163212 };
164213
165214 return vulnsManager;
22 // See the file 'doc/LICENSE' for the license information
33
44 angular.module('faradayApp')
5 .controller('workspacesCtrl', ['$modal', '$scope', '$q', 'workspacesFact', 'dashboardSrv',
6 function($modal, $scope, $q, workspacesFact, dashboardSrv) {
5 .controller('workspacesCtrl', ['$uibModal', '$scope', '$q', 'workspacesFact', 'dashboardSrv',
6 function($uibModal, $scope, $q, workspacesFact, dashboardSrv) {
77 $scope.hash;
88 $scope.objects;
99 $scope.workspaces;
6969 };
7070
7171 $scope.onFailInsert = function(error){
72 var modal = $modal.open({
72 var modal = $uibModal.open({
7373 templateUrl: 'scripts/commons/partials/modalKO.html',
7474 controller: 'commonsModalKoCtrl',
7575 resolve: {
157157
158158 // Modals methods
159159 $scope.new = function(){
160 $scope.modal = $modal.open({
160 $scope.modal = $uibModal.open({
161161 templateUrl: 'scripts/workspaces/partials/modalNew.html',
162162 controller: 'workspacesModalNew',
163163 size: 'lg'
179179 });
180180
181181 if(workspace){
182 var modal = $modal.open({
182 var modal = $uibModal.open({
183183 templateUrl: 'scripts/workspaces/partials/modalEdit.html',
184184 controller: 'workspacesModalEdit',
185185 size: 'lg',
196196 }
197197 });
198198 } else {
199 var modal = $modal.open({
199 var modal = $uibModal.open({
200200 templateUrl: 'scripts/commons/partials/modalKO.html',
201201 controller: 'commonsModalKoCtrl',
202202 resolve: {
220220 });
221221
222222 if(selected) {
223 $scope.modal = $modal.open({
223 $scope.modal = $uibModal.open({
224224 templateUrl: 'scripts/commons/partials/modalDelete.html',
225225 controller: 'commonsModalDelete',
226226 size: 'lg',
239239 });
240240 });
241241 } else {
242 var modal = $modal.open({
242 var modal = $uibModal.open({
243243 templateUrl: 'scripts/commons/partials/modalKO.html',
244244 controller: 'commonsModalKoCtrl',
245245 resolve: {
2424 <h5>Start Date</h5>
2525 <label class="sr-only" for="work-start">Start Date</label>
2626 <p class="input-group">
27 <input type="text" class="form-control" datepicker-popup="MM/dd/yyyy" ng-model="workspace.duration.start" is-open="openedStart" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="Start Date"/>
27 <input type="text" class="form-control" uib-datepicker-popup="MM/dd/yyyy" ng-model="workspace.duration.start" is-open="openedStart" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="Start Date"/>
2828 <span class="input-group-btn">
2929 <button type="button" class="btn btn-default" ng-click="open($event,true)"><i class="glyphicon glyphicon-calendar"></i></button>
3030 </span>
3434 <h5>End Date</h5>
3535 <label class="sr-only" for="work-end">End Date</label>
3636 <p class="input-group">
37 <input type="text" class="form-control" datepicker-popup="MM/dd/yyyy" ng-model="workspace.duration.end" is-open="openedEnd" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="End Date"/>
37 <input type="text" class="form-control" uib-datepicker-popup="MM/dd/yyyy" ng-model="workspace.duration.end" is-open="openedEnd" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="End Date"/>
3838 <span class="input-group-btn">
3939 <button type="button" class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
4040 </span>
4343 <div class="col-md-6 datepicker">
4444 <label class="sr-only" for="work-start">Start Date</label>
4545 <p class="input-group">
46 <input type="text" class="form-control" datepicker-popup="MM/dd/yyyy" ng-model="workspace.start" is-open="openedStart" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="Start Date" />
46 <input type="text" class="form-control" uib-datepicker-popup="MM/dd/yyyy" ng-model="workspace.start" is-open="openedStart" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="Start Date" />
4747 <span class="input-group-btn">
4848 <button type="button" class="btn btn-default" ng-click="open($event,true)"><i class="glyphicon glyphicon-calendar"></i></button>
4949 </span>
5252 <div class="col-md-6 datepicker">
5353 <label class="sr-only" for="work-end">End Date</label>
5454 <p class="input-group">
55 <input type="text" class="form-control" datepicker-popup="MM/dd/yyyy" ng-model="workspace.end" is-open="openedEnd" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="End Date" />
55 <input type="text" class="form-control" uib-datepicker-popup="MM/dd/yyyy" ng-model="workspace.end" is-open="openedEnd" datepicker-options="dateOptions" date-disabled="disabled(date, mode)" close-text="Close" placeholder="End Date" />
5656 <span class="input-group-btn">
5757 <button type="button" class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
5858 </span>
1919 filterReservedWorkspaces = function(wss) {
2020 var deferred = $q.defer();
2121 deferred.resolve(wss.data.filter(function(ws) {
22 return ws.search(/^_/) < 0 && ws.search("cwe") < 0 && ws.search("reports") < 0;
22 return ws.search(/^_/) < 0 && ws.search("^cwe$") < 0 && ws.search("^reports$") < 0;
2323 }));
2424 return deferred.promise;
2525 };
1313 emit("interfaces", 1);
1414 } else if(doc.type=="Note") {
1515 emit("notes", 1);
16 } else if(doc.type=="VulnerabilityWeb" || doc.type=="Vulnerability") {
17 if(doc.type=="VulnerabilityWeb") {
18 emit("web vulns", 1);
19 } else {
20 emit("vulns", 1);
21 }
22 emit("total vulns", 1);
2316 }
2417 }
99 "confidentiality": false,
1010 "integrity": false
1111 },
12 resolution = "";
12 resolution = "",
13 confirmed = false;
1314 if(doc.easeofresolution !== undefined) {
1415 easeofresolution = doc.easeofresolution;
1516 }
2425 "_id": doc._id,
2526 "_rev": doc._rev,
2627 "_attachments": doc._attachments,
28 "confirmed": doc.confirmed || confirmed,
2729 "data": doc.data,
2830 "desc": doc.desc,
2931 "easeofresolution": easeofresolution,
44 #
55 #'''
66
7
87 WORKSPACE=`cat $HOME/.faraday/config/user.xml | grep '<last_workspace' | cut -d '>' -f 2 | cut -d '<' -f 1`
9 STATUS=`curl -s 127.0.0.1:9977/status/check | sed "s/[^0-9]//g" | grep -v '^[[:space:]]*$'`
8 STATUS=`curl -s $FARADAY_ZSH_HOST:$FARADAY_ZSH_RPORT/status/check | sed "s/[^0-9]//g" | grep -v '^[[:space:]]*$'`
109 PS1="%{${fg_bold[red]}%}[faraday]($WORKSPACE)%{${reset_color}%} $PS1"
1110
1211 echo ">>> WELCOME TO FARADAY"
1312 echo "[+] Current Workspace: $WORKSPACE"
1413 if [[ -z $STATUS ]]; then
1514 echo "[-] API: Warning API unreachable"
16
17 elif [[ $STATUS == "200" ]]; then
15
16 elif [[ $STATUS == "200" ]]; then
1817 echo "[+] API: OK"
19 else
20 echo "[!] API: $STATUS"
21
18 else
19 echo "[!] API: $STATUS"
20
2221 fi
2322
2423 setopt multios
1919 if not os.path.exists(output_folder):
2020 os.mkdir(output_folder)
2121
22 #TODO: Load this from faraday config
23 host = "127.0.0.1"
24 port = 9977
22 host = os.environ["FARADAY_ZSH_HOST"]
23 port = int(os.environ["FARADAY_ZSH_RPORT"])
2524
2625 url_input = "http://%s:%d/cmd/input" % (host, port)
2726 url_output = "http://%s:%d/cmd/output" % (host, port)