Commit 51c56980 authored by Randall Leeds's avatar Randall Leeds

Merge branch '719-stream-and-search-2' into develop

parents 00c8b946 9cc0e7a7
......@@ -642,6 +642,15 @@ blockquote {
display: inline-block;
font-family: $sansFontFamily;
}
&.search-upper {
padding: 0em;
padding-top: .15em;
}
}
.right-border {
padding-right: 3.7em;
}
}
......
imports = [
'bootstrap'
'ngRoute'
'h.controllers'
'h.directives'
'h.app_directives'
......
......@@ -384,6 +384,26 @@ fuzzytime = ['$filter', '$window', ($filter, $window) ->
template: '<span class="small">{{ftime | date:mediumDate}}</span>'
]
streamviewer = [ ->
link: (scope, elem, attr, ctrl) ->
return unless ctrl?
require: '?ngModel'
restrict: 'E'
templateUrl: 'streamviewer.html'
]
whenscrolled = ['$window', ($window) ->
link: (scope, elem, attr) ->
$window = angular.element($window)
$window.on 'scroll', ->
windowBottom = $window.height() + $window.scrollTop()
elementBottom = elem.offset().top + elem.height()
remaining = elementBottom - windowBottom
shouldScroll = remaining <= $window.height() * 0
if shouldScroll
scope.$apply attr.whenscrolled
]
angular.module('h.directives', ['ngSanitize'])
.directive('authentication', authentication)
......@@ -401,4 +421,5 @@ angular.module('h.directives', ['ngSanitize'])
.directive('ngBlur', ngBlur)
.directive('repeatAnim', repeatAnim)
.directive('notification', notification)
.directive('streamviewer', streamviewer)
.directive('whenscrolled', whenscrolled)
......@@ -39,25 +39,28 @@ class FlashProvider
]
flashInterceptor = ['$q', 'flash', ($q, flash) ->
response: (response) ->
data = response.data
format = response.headers 'content-type'
if format?.match /^application\/json/
if data.flash?
flash q, msgs for q, msgs of data.flash
if data.status is 'failure'
flash 'error', data.reason
$q.reject(data.reason)
else if data.status is 'okay'
response.data = data.model
response
else
response
]
angular.module('h.flash', ['ngResource'])
.provider('flash', FlashProvider)
.factory('flashInterceptor', flashInterceptor)
.config(['$httpProvider', ($httpProvider) ->
$httpProvider.responseInterceptors.push ['$q', 'flash', ($q, flash) ->
(promise) ->
promise.then (response) ->
data = response.data
format = response.headers 'content-type'
if format?.match /^application\/json/
if data.flash?
flash q, msgs for q, msgs of data.flash
if data.status is 'failure'
flash 'error', data.reason
$q.reject(data.reason)
else if data.status is 'okay'
response.data = data.model
response
else
response
]
$httpProvider.interceptors.push 'flashInterceptor'
])
\ No newline at end of file
get_quote = (annotation) ->
if annotation.quote? then return annotation.quote
if not 'target' in annotation then return ''
quote = '(Reply annotation)'
for target in annotation['target']
for selector in target['selector']
if selector['type'] is 'TextQuoteSelector'
quote = selector['exact'] + ' '
quote
class Stream
path: window.location.protocol + '//' + window.location.hostname + ':' +
window.location.port + '/__streamer__'
this.$inject = ['$location','$scope','$timeout','streamfilter']
constructor: ($location, $scope, $timeout, streamfilter) ->
$scope.annotations = []
urlParts = $location.absUrl().split('/')
$scope.filterValue = urlParts.pop()
filterType = urlParts.pop()
if filterType == "t"
$scope.filterDescription = "Annotations with tag '#{ $scope.filterValue }'"
filterClause = 'tags:i#' + $scope.filterValue
else
$scope.filterDescription = "Annotations by user '#{ $scope.filterValue }'"
filterClause = 'user:i=' + $scope.filterValue
# Generate client ID
buffer = new Array(16)
uuid.v4 null, buffer, 0
@clientID = uuid.unparse buffer
$scope.filter =
streamfilter
.setPastDataHits(150)
.setMatchPolicyIncludeAny()
.setClausesParse(filterClause)
.getFilter()
$scope.manage_new_data = (data, action) =>
for annotation in data
annotation.action = action
annotation.quote = get_quote annotation
annotation._share_link = window.location.protocol +
'//' + window.location.hostname + ':' + window.location.port + "/a/" + annotation.id
annotation._anim = 'fade'
switch action
when 'create', 'past'
unless annotation in $scope.annotations
$scope.annotations.unshift annotation
when 'update'
index = 0
for ann in $scope.annotations
if ann.id is annotation.id
# Remove the original
$scope.annotations.splice index,1
# Put back the edited
$scope.annotations.unshift annotation
break
index +=1
when 'delete'
for ann in $scope.annotations
if ann.id is annotation.id
$scope.annotations.splice index,1
break
index +=1
$scope.open = =>
$scope.sock = new SockJS(@path)
$scope.sock.onopen = =>
sockmsg =
filter: $scope.filter
clientID: @clientID
$scope.sock.send JSON.stringify sockmsg
$scope.sock.onclose = =>
$timeout $scope.open, 5000
$scope.sock.onmessage = (msg) =>
console.log 'Got something'
console.log msg
unless msg.data.type? and msg.data.type is 'annotation-notification'
return
data = msg.data.payload
action = msg.data.options.action
unless data instanceof Array then data = [data]
$scope.$apply =>
$scope.manage_new_data data, action
$scope.open()
angular.module('h.stream',['h.streamfilter', 'h.filters','h.directives','bootstrap'])
.controller('StreamCtrl', Stream)
get_quote = (annotation) ->
if not 'target' in annotation then return ''
quote = '(This is a reply annotation)'
for target in annotation['target']
for selector in target['selector']
if selector['type'] is 'TextQuoteSelector'
quote = selector['exact'] + ' '
quote
syntaxHighlight = (json) ->
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) ->
cls = 'number'
if /^"/.test(match)
if /:$/.test(match) then cls = 'key'
else cls = 'string'
else
if /true|false/.test(match) then cls = 'boolean'
else
if /null/.test(match) then cls = 'null'
return '<span class="' + cls + '">' + match + '</span>'
)
class Streamer
path: window.location.protocol + '//' + window.location.hostname + ':' +
window.location.port + '/__streamer__'
strategies: ['include_any', 'include_all', 'exclude_any', 'exclude_all']
past_modes: ['none','hits','time']
this.$inject = ['$location', '$scope', 'streamfilter', 'clauseparser']
constructor: ($location, $scope, streamfilter, clauseparser) ->
$scope.streaming = false
$scope.annotations = []
$scope.bads = []
$scope.time = 5
$scope.hits = 100
@sfilter = streamfilter
@sfilter.setPastDataHits(100)
$scope.filter = @sfilter.filter
#parse for route params
params = $location.search()
if params.match_policy in @strategies
$scope.filter.match_policy = params.match_policy
if params.action_create
if (typeof params.action_create) is 'boolean'
@sfilter.setActionCreate(params.action_create)
else
@sfilter.setActionCreate(params.action_create is 'true')
if params.action_update
if (typeof params.action_update) is 'boolean'
@sfilter.setActionUpdate(params.action_update)
else
@sfilter.setActionUpdate(params.action_update is 'true')
if params.action_delete
if (typeof params.action_delete) is 'boolean'
@sfilter.setActionDelete(params.action_delete)
else
@sfilter.setActionDelete(params.action_delete is 'true')
if params.load_past in @past_modes
if params.hits? and parseInt(params.hits) is not NaN
@sfilter.setPastDataHits(parseInt(params.hits))
if params.go_back? and parseInt(params.go_back) is not NaN
@sfilter.setPastDataTime(parseInt(params.go_back))
if params.clauses
test_clauses = params.clauses.replace ",", " "
@sfilter.setClausesParse(test_clauses)
$scope.clauses = test_clauses
else
$scope.clauses = ''
console.log $scope.filter
$scope.toggle_past = ->
switch $scope.filter.past_data.load_past
when 'none' then @sfilter.setPastDataTime($scope.time)
when 'time' then @sfilter.setPastDataHits($scope.hits)
when 'hits' then @sfilter.setPastDataNone()
$scope.$watch 'filter', (newValue, oldValue) =>
json = JSON.stringify $scope.filter, undefined, 2
$scope.json_content = syntaxHighlight json
,true
$scope.clause_change = =>
if $scope.clauses.slice(-1) is ' ' or $scope.clauses.length is 0
res = clauseparser.parse_clauses($scope.clauses)
if res?
$scope.filter.clauses = res[0]
$scope.bads = res[1]
else
$scope.filter.clauses = []
$scope.bads = []
$scope.start_streaming = =>
if $scope.streaming
$scope.sock.close()
$scope.streaming = false
res = clauseparser.parse_clauses($scope.clauses)
if res
$scope.filter.clauses = res[0]
$scope.bads = res[1]
unless $scope.bads.length is 0
return
$scope.open()
$scope.open = =>
$scope.sock = new SockJS @path
$scope.sock.onopen = =>
$scope.sock.send JSON.stringify $scope.filter
$scope.streaming = true
$scope.sock.onclose = =>
$scope.streaming = false
$scope.sock.onmessage = (msg) =>
console.log 'Got something'
console.log msg
data = msg.data[0]
action = msg.data[1]
unless data instanceof Array then data = [data]
$scope.$apply =>
$scope.manage_new_data data, action
$scope.manage_new_data = (data, action) =>
for annotation in data
annotation.action = action
annotation.quote = get_quote annotation
$scope.annotations.splice 0,0,annotation
#Update the parameters
$location.search
'match_policy': $scope.filter.match_policy
'action_create': $scope.filter.actions.create
'action_update': $scope.filter.actions.update
'action_delete': $scope.filter.actions.delete
'load_past': $scope.filter.past_data.load_past
'go_back': $scope.filter.past_data.go_back
'hits': $scope.filter.past_data.hits
'clauses' : $scope.clauses.replace " ", ","
$scope.stop_streaming = ->
$scope.sock.close()
$scope.streaming = false
angular.module('h.streamer',['h.streamfilter','h.filters','bootstrap'])
.controller('StreamerCtrl', Streamer)
class ClauseParser
filter_fields : ['references', 'text', 'user','uri', 'id', 'tags']
operators: ['=', '>', '<', '=>', '>=', '<=', '=<', '[', '#', '^']
filter_fields : ['references', 'text', 'user', 'uri', 'id', 'tags', 'created', 'updated']
operators: ['=','=>', '>=', '<=', '=<', '>', '<', '[', '#', '^', '{']
operator_mapping:
'=': 'equals'
'>': 'gt'
'<': 'lt'
'=>' : 'ge'
'>=' : 'ge'
'=>': 'ge'
'>=': 'ge'
'=<': 'le'
'<=' : 'le'
'<=': 'le'
'[' : 'one_of'
'#' : 'matches'
'^' : 'first_of'
'{' : 'match_of' # one_of but not exact search
insensitive_operator : 'i'
parse_clauses: (clauses) ->
......@@ -155,12 +156,13 @@ class StreamFilter
@filter.clauses.push clause
this
addClause: (field, operator, value, case_sensitive = false) ->
addClause: (field, operator, value, case_sensitive = false, es_query_string = false) ->
@filter.clauses.push
field: field
operator: operator
value: value
case_sensitive: case_sensitive
es_query_string: es_query_string
this
setClausesParse: (clauses_to_parse, error_checking = false) ->
......
get_quote = (annotation) ->
if annotation.quote? then return annotation.quote
if not 'target' in annotation then return ''
quote = '(Reply annotation)'
for target in annotation['target']
for selector in target['selector']
if selector['type'] is 'TextQuoteSelector'
quote = selector['exact'] + ' '
quote
# This class will process the results of search and generate the correct filter
# It expects the following dict format as rules
# { facet_name : {
# formatter: to format the value (optional)
# path: json path mapping to the annotation field
# exact_match: true|false (default: true)
# case_sensitive: true|false (default: false)
# and_or: and|or for multiple values should it threat them as 'or' or 'and' (def: or)
# es_query_string: should the streaming backend use query_string es query for this facet
# operator: if given it'll use this operator regardless of other circumstances
# }
# The models is the direct output from visualsearch
# The limit is the default limit
class SearchHelper
populateFilter: (filter, models, rules, limit = 50) ->
# First cluster the different facets into categories
categories = {}
for searchItem in models
category = searchItem.attributes.category
value = searchItem.attributes.value
if category is 'results' then limit = value
else
if category is 'text'
# Visualsearch sickly automatically cluster the text field
# (and only the text filed) into a space separated string
catlist = []
catlist.push val for val in value.split ' '
categories[category] = catlist
else
if category of categories then categories[category].push value
else categories[category] = [value]
filter.setPastDataHits(limit)
# Now for the categories
for category, values of categories
unless rules[category]? then continue
unless values.length then continue
rule = rules[category]
# Now generate the clause with the help of the rule
exact_match = if rule.exact_match? then rule.exact_match else true
case_sensitive = if rule.case_sensitive? then rule.case_sensitive else false
and_or = if rule.and_or? then rule.and_or else 'or'
mapped_field = if rule.path? then rule.path else '/'+category
es_query_string = if rule.es_query_string? then rule.es_query_string else false
if values.length is 1
oper_part =
if rule.operator? then rule.operator
else if exact_match then 'equals' else 'matches'
value_part = if rule.formatter then rule.formatter values[0] else values[0]
filter.addClause mapped_field, oper_part, value_part, case_sensitive, es_query_string
else
if and_or is 'or'
val_list = ''
first = true
for val in values
unless first then val_list += ',' else first = false
value_part = if rule.formatter then rule.formatter val else val
val_list += value_part
oper_part =
if rule.operator? then rule.operator
else if exact_match then 'one_of' else 'match_of'
filter.addClause mapped_field, oper_part, val_list, case_sensitive, es_query_string
else
oper_part =
if rule.operator? then rule.operator
else if exact_match then 'equals' else 'matches'
for val in values
value_part = if rule.formatter then rule.formatter val else val
filter.addClause mapped_field, oper_part, value_part, case_sensitive, es_query_string
if limit != 50 then categories['results'] = [limit]
[filter.getFilter(), categories]
class StreamSearch
facets: ['text','tags', 'uri', 'quote','since','user','results']
rules:
user:
formatter: (user) ->
'acct:' + user + '@' + window.location.hostname
path: '/user'
exact_match: true
case_sensitive: false
and_or: 'or'
text:
path: '/text'
exact_match: false
case_sensitive: false
and_or: 'and'
tags:
path: '/tags'
exact_match: false
case_sensitive: false
and_or: 'or'
quote:
path: "/quote"
exact_match: false
case_sensitive: false
and_or: 'and'
uri:
formatter: (uri) ->
uri = uri.toLowerCase()
if uri.match(/http:\/\//) then uri = uri.substring(7)
if uri.match(/https:\/\//) then uri = uri.substring(8)
if uri.match(/^www\./) then uri = uri.substring(4)
uri
path: '/uri'
exact_match: false
case_sensitive: false
es_query_string: true
and_or: 'or'
since:
formatter: (past) ->
seconds =
switch past
when '5 min' then 5*60
when '30 min' then 30*60
when '1 hour' then 60*60
when '12 hours' then 12*60*60
when '1 day' then 24*60*60
when '1 week' then 7*24*60*60
when '1 month' then 30*24*60*60
when '1 year' then 365*24*60*60
new Date(new Date().valueOf() - seconds*1000)
path: '/created'
exact_match: false
case_sensitive: true
and_or: 'and'
operator: 'ge'
this.inject = ['$element', '$location', '$scope', '$timeout', 'streamfilter']
constructor: (
$element, $location, $scope, $timeout, streamfilter
) ->
$scope.path = window.location.protocol + '//' + window.location.hostname + ':' +
window.location.port + '/__streamer__'
$scope.empty = false
# Generate client ID
buffer = new Array(16)
uuid.v4 null, buffer, 0
@clientID = uuid.unparse buffer
$scope.sortAnnotations = (a, b) ->
a_upd = if a.updated? then new Date(a.updated) else new Date()
b_upd = if b.updated? then new Date(b.updated) else new Date()
a_upd.getTime() - b_upd.getTime()
# Read search params
search_query = ''
params = $location.search()
for param, values of params
# Ignore non facet parameters
if param in @facets
unless values instanceof Array then values = [values]
for value in values
search_query += param + ': "' + value + '" '
# Initialize Visual search
@search = VS.init
container: $element.find('.visual-search')
query: search_query
callbacks:
search: (query, searchCollection) =>
# Assemble the filter json
filter =
streamfilter
.setMatchPolicyIncludeAll()
.noClauses()
[filter, $scope.categories] =
new SearchHelper().populateFilter filter, searchCollection.models, @rules
$scope.initStream filter
# Update the parameters
$location.search $scope.categories
facetMatches: (callback) =>
# Created and limit should be singleton.
add_limit = true
add_created = true
for facet in @search.searchQuery.facets()
if facet.hasOwnProperty 'results' then add_limit = false
if facet.hasOwnProperty 'since' then add_created = false
if add_limit and add_created then list = ['text','tags', 'uri', 'quote','since','user','results']
else
if add_limit then list = ['text','tags', 'uri', 'quote','user', 'results']
else
if add_created then list = ['text','tags', 'uri', 'quote','since','user']
else list = ['text','tags', 'uri', 'quote','user']
return callback list, {preserveOrder: true}
valueMatches: (facet, searchTerm, callback) ->
switch facet
when 'results'
callback ['0', '10', '25', '50', '100', '250', '1000']
when 'since'
callback ['5 min', '30 min', '1 hour', '12 hours', '1 day', '1 week', '1 month', '1 year'], {preserveOrder: true}
clearSearch: (original) =>
# Execute clearSearch's internal method for resetting search
original()
$scope.$apply ->
$scope.annotations = []
$scope.empty = false
$location.search {}
$scope.initStream = (filter) ->
if $scope.sock? then $scope.sock.close()
$scope.annotations = new Array()
$scope.sock = new SockJS($scope.path)
$scope.sock.onopen = =>
sockmsg =
filter: filter
clientID: @clientID
$scope.sock.send JSON.stringify sockmsg
$scope.sock.onclose = =>
# stream is closed
$scope.sock.onmessage = (msg) =>
console.log 'Got something'
console.log msg
unless msg.data.type? and msg.data.type is 'annotation-notification'
return
data = msg.data.payload
action = msg.data.options.action
unless data instanceof Array then data = [data]
if data.length
$scope.$apply =>
$scope.empty = false
$scope.manage_new_data data, action
else
unless $scope.annotations.length
$scope.$apply =>
$scope.empty = true
$scope.manage_new_data = (data, action) =>
for annotation in data
annotation.action = action
annotation.quote = get_quote annotation
annotation._share_link = window.location.protocol +
'//' + window.location.hostname + ':' + window.location.port + "/a/" + annotation.id
annotation._anim = 'fade'
if annotation in $scope.annotations then continue
switch action
when 'create', 'past'
unless annotation in $scope.annotations
$scope.annotations.unshift annotation
when 'update'
index = 0
found = false
for ann in $scope.annotations
if ann.id is annotation.id
# Remove the original
$scope.annotations.splice index,1
# Put back the edited
$scope.annotations.unshift annotation
found = true
break
index +=1
# Sometimes editing an annotation makes it appear in the list
# If it wasn't part of it before. (i.e. adding a new tag)
unless found
$scope.annotations.unshift annotation
when 'delete'
index = 0
for ann in $scope.annotations
if ann.id is annotation.id
$scope.annotations.splice index,1
break
index +=1
$scope.annotations = $scope.annotations.sort($scope.sortAnnotations).reverse()
$scope.loadMore = (number) =>
console.log 'loadMore'
unless $scope.sock? then return
sockmsg =
messageType: 'more_hits'
clientID: @clientID
moreHits: number
$scope.sock.send JSON.stringify sockmsg
$scope.annotations = []
$timeout =>
@search.searchBox.app.options.callbacks.search @search.searchBox.value(), @search.searchBox.app.searchQuery
,500
angular.module('h.streamsearch',['h.streamfilter','h.filters','h.directives','bootstrap'])
.controller('StreamSearchController', StreamSearch)
/**
* @license AngularJS v1.1.4
* @license AngularJS v1.2.0-rc.2
* (c) 2010-2012 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {
'use strict';
(function(window, angular, undefined) {'use strict';
var $resourceMinErr = angular.$$minErr('$resource');
/**
* @ngdoc overview
* @name ngResource
* @description
*
* # ngResource
*
* `ngResource` is the name of the optional Angular module that adds support for interacting with
* [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
* `ngReource` provides the {@link ngResource.$resource `$resource`} serivce.
*
* {@installModule resource}
*
* See {@link ngResource.$resource `$resource`} for usage.
*/
/**
......@@ -24,19 +35,18 @@
* The returned resource object has action methods which provide high-level behaviors without
* the need to interact with the low level {@link ng.$http $http} service.
*
* # Installation
* To use $resource make sure you have included the `angular-resource.js` that comes in Angular
* package. You also can find this stuff in {@link http://code.angularjs.org/ code.angularjs.org}.
* Finally load the module in your application:
*
* angular.module('app', ['ngResource']);
*
* and you ready to get started!
* Requires the {@link ngResource `ngResource`} module to be installed.
*
* @param {string} url A parametrized URL template with parameters prefixed by `:` as in
* `/user/:username`. If you are using a URL with a port number (e.g.
* `http://example.com:8080/api`), you'll need to escape the colon character before the port
* number, like this: `$resource('http://example.com\\:8080/api')`.
* `http://example.com:8080/api`), it will be respected.
*
* If you are using a url with a suffix, just add the suffix, like this:
* `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')`
* or even `$resource('http://example.com/resource/:resource_id.:format')`
* If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be
* collapsed down to a single `.`. If you need this sequence to appear and not collapse then you
* can escape it with `/\.`.
*
* @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
* `actions` methods. If any of the parameter value is a function, it will be executed every time
......@@ -82,12 +92,16 @@
* GET request, otherwise if a cache instance built with
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
* caching.
* - **`timeout`** – `{number}` – timeout in milliseconds.
* - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that
* should abort the request when resolved.
* - **`withCredentials`** - `{boolean}` - whether to to set the `withCredentials` flag on the
* XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5
* requests with credentials} for more information.
* - **`responseType`** - `{string}` - see {@link
* https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}.
* - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
* `response` and `responseError`. Both `response` and `responseError` interceptors get called
* with `http response` object. See {@link ng.$http $http interceptors}.
*
* @returns {Object} A resource "class" object with methods for the default set of resource actions
* optionally extended with custom `actions`. The default set contains these actions:
......@@ -126,24 +140,27 @@
* - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
* - non-GET instance actions: `instance.$action([parameters], [success], [error])`
*
* Success callback is called with (value, responseHeaders) arguments. Error callback is called
* with (httpResponse) argument.
*
* The Resource instances and collection have these additional properties:
* Class actions return empty instance (with additional properties below).
* Instance actions return promise of the action.
*
* - `$then`: the `then` method of a {@link ng.$q promise} derived from the underlying
* {@link ng.$http $http} call.
* The Resource instances and collection have these additional properties:
*
* The success callback for the `$then` method will be resolved if the underlying `$http` requests
* succeeds.
* - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
* instance or collection.
*
* The success callback is called with a single object which is the {@link ng.$http http response}
* object extended with a new property `resource`. This `resource` property is a reference to the
* result of the resource action — resource object or array of resources.
* On success, the promise is resolved with the same resource instance or collection object,
* updated with data from server. This makes it easy to use in
* {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view rendering
* until the resource(s) are loaded.
*
* The error callback is called with the {@link ng.$http http response} object when an http
* error occurs.
* On failure, the promise is resolved with the {@link ng.$http http response} object,
* without the `resource` property.
*
* - `$resolved`: true if the promise has been resolved (either with success or rejection);
* Knowing if the Resource has been resolved is useful in data-binding.
* - `$resolved`: `true` after first server interaction is completed (either with success or rejection),
* `false` before that. Knowing if the Resource has been resolved is useful in data-binding.
*
* @example
*
......@@ -264,7 +281,7 @@
</doc:example>
*/
angular.module('ngResource', ['ng']).
factory('$resource', ['$http', '$parse', function($http, $parse) {
factory('$resource', ['$http', '$parse', '$q', function($http, $parse, $q) {
var DEFAULT_ACTIONS = {
'get': {method:'GET'},
'save': {method:'POST'},
......@@ -321,7 +338,7 @@ angular.module('ngResource', ['ng']).
}
function Route(template, defaults) {
this.template = template = template + '#';
this.template = template;
this.defaults = defaults || {};
this.urlParams = {};
}
......@@ -335,7 +352,7 @@ angular.module('ngResource', ['ng']).
var urlParams = self.urlParams = {};
forEach(url.split(/\W/), function(param){
if (param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
if (!(new RegExp("^\\d+$").test(param)) && param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
urlParams[param] = true;
}
});
......@@ -359,8 +376,14 @@ angular.module('ngResource', ['ng']).
}
});
// set the url
config.url = url.replace(/\/?#$/, '').replace(/\/*$/, '');
// strip trailing slashes and set the url
url = url.replace(/\/+$/, '');
// then replace collapse `/.` if found in the last URL path segment before the query
// E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x`
url = url.replace(/\/\.(?=\w+($|\?))/, '.');
// replace escaped `/\.` with `/.`
config.url = url.replace(/\/\\\./, '/.');
// set params - delegate param encoding to $http
forEach(params, function(value, key){
......@@ -383,24 +406,24 @@ angular.module('ngResource', ['ng']).
actionParams = extend({}, paramDefaults, actionParams);
forEach(actionParams, function(value, key){
if (isFunction(value)) { value = value(); }
ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
ids[key] = value && value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
});
return ids;
}
function defaultResponseInterceptor(response) {
return response.resource;
}
function Resource(value){
copy(value || {}, this);
}
forEach(actions, function(action, name) {
action.method = angular.uppercase(action.method);
var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);
Resource[name] = function(a1, a2, a3, a4) {
var params = {};
var data;
var success = noop;
var error = null;
var promise;
var params = {}, data, success, error;
switch(arguments.length) {
case 4:
......@@ -432,33 +455,35 @@ angular.module('ngResource', ['ng']).
break;
case 0: break;
default:
throw "Expected between 0-4 arguments [params, data, success, error], got " +
arguments.length + " arguments.";
throw $resourceMinErr('badargs',
"Expected up to 4 arguments [params, data, success, error], got {0} arguments", arguments.length);
}
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
var httpConfig = {},
promise;
var isInstanceCall = data instanceof Resource;
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
var httpConfig = {};
var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor;
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || undefined;
forEach(action, function(value, key) {
if (key != 'params' && key != 'isArray' ) {
if (key != 'params' && key != 'isArray' && key != 'interceptor') {
httpConfig[key] = copy(value);
}
});
httpConfig.data = data;
route.setUrlParams(httpConfig, extend({}, extractParams(data, action.params || {}), params), action.url);
function markResolved() { value.$resolved = true; }
promise = $http(httpConfig);
value.$resolved = false;
promise.then(markResolved, markResolved);
value.$then = promise.then(function(response) {
var data = response.data;
var then = value.$then, resolved = value.$resolved;
var promise = $http(httpConfig).then(function(response) {
var data = response.data,
promise = value.$promise;
if (data) {
if ( angular.isArray(data) != !!action.isArray ) {
throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected response' +
' to contain an {0} but got an {1}',
action.isArray?'array':'object', angular.isArray(data)?'array':'object');
}
if (action.isArray) {
value.length = 0;
forEach(data, function(item) {
......@@ -466,44 +491,47 @@ angular.module('ngResource', ['ng']).
});
} else {
copy(data, value);
value.$then = then;
value.$resolved = resolved;
value.$promise = promise;
}
}
value.$resolved = true;
(success||noop)(value, response.headers);
response.resource = value;
return response;
}, error).then;
}, function(response) {
value.$resolved = true;
return value;
};
(error||noop)(response);
return $q.reject(response);
}).then(responseInterceptor, responseErrorInterceptor);
Resource.prototype['$' + name] = function(a1, a2, a3) {
var params = extractParams(this),
success = noop,
error;
switch(arguments.length) {
case 3: params = a1; success = a2; error = a3; break;
case 2:
case 1:
if (isFunction(a1)) {
success = a1;
error = a2;
} else {
params = a1;
success = a2 || noop;
}
case 0: break;
default:
throw "Expected between 1-3 arguments [params, success, error], got " +
arguments.length + " arguments.";
if (!isInstanceCall) {
// we are creating instance / collection
// - set the initial promise
// - return the instance / collection
value.$promise = promise;
value.$resolved = false;
return value;
}
// instance call
return promise;
};
Resource.prototype['$' + name] = function(params, success, error) {
if (isFunction(params)) {
error = success; success = params; params = {};
}
var data = hasBody ? this : undefined;
Resource[name].call(this, params, data, success, error);
var result = Resource[name](params, this, success, error);
return result.$promise || result;
};
});
......
/**
* @license AngularJS v1.2.0-rc.2
* (c) 2010-2012 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
var copy = angular.copy,
equals = angular.equals,
extend = angular.extend,
forEach = angular.forEach,
isDefined = angular.isDefined,
isFunction = angular.isFunction,
isString = angular.isString,
jqLite = angular.element,
noop = angular.noop,
toJson = angular.toJson;
function inherit(parent, extra) {
return extend(new (extend(function() {}, {prototype:parent}))(), extra);
}
/**
* @ngdoc overview
* @name ngRoute
* @description
*
* # ngRoute
*
* The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
*
* {@installModule route}
*
*/
var ngRouteModule = angular.module('ngRoute', ['ng']).
provider('$route', $RouteProvider);
/**
* @ngdoc object
* @name ngRoute.$routeProvider
* @function
*
* @description
*
* Used for configuring routes. See {@link ngRoute.$route $route} for an example.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*/
function $RouteProvider(){
var routes = {};
/**
* @ngdoc method
* @name ngRoute.$routeProvider#when
* @methodOf ngRoute.$routeProvider
*
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
* contains redundant trailing slash or is missing one, the route will still match and the
* `$location.path` will be updated to add or drop the trailing slash to exactly match the
* route definition.
*
* * `path` can contain named groups starting with a colon (`:name`). All characters up
* to the next slash are matched and stored in `$routeParams` under the given `name`
* when the route matches.
* * `path` can contain named groups starting with a colon and ending with a star (`:name*`).
* All characters are eagerly stored in `$routeParams` under the given `name`
* when the route matches.
* * `path` can contain optional named groups with a question mark (`:name?`).
*
* For example, routes like `/color/:color/largecode/:largecode*\/edit` will match
* `/color/brown/largecode/code/with/slashs/edit` and extract:
*
* * `color: brown`
* * `largecode: code/with/slashs`.
*
*
* @param {Object} route Mapping information to be assigned to `$route.current` on route
* match.
*
* Object properties:
*
* - `controller` – `{(string|function()=}` – Controller fn that should be associated with newly
* created scope or the name of a {@link angular.Module#controller registered controller}
* if passed as a string.
* - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be
* published to scope under the `controllerAs` name.
* - `template` – `{string=|function()=}` – html template as a string or a function that
* returns an html template as a string which should be used by {@link
* ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives.
* This property takes precedence over `templateUrl`.
*
* If `template` is a function, it will be called with the following parameters:
*
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
* - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
* template that should be used by {@link ngRoute.directive:ngView ngView}.
*
* If `templateUrl` is a function, it will be called with the following parameters:
*
* - `{Array.<Object>}` - route parameters extracted from the current
* `$location.path()` by applying the current route
*
* - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
* be injected into the controller. If any of these dependencies are promises, they will be
* resolved and converted to a value before the controller is instantiated and the
* `$routeChangeSuccess` event is fired. The map object is:
*
* - `key` – `{string}`: a name of a dependency to be injected into the controller.
* - `factory` - `{string|function}`: If `string` then it is an alias for a service.
* Otherwise if function, then it is {@link api/AUTO.$injector#invoke injected}
* and the return value is treated as the dependency. If the result is a promise, it is resolved
* before its value is injected into the controller. Be aware that `ngRoute.$routeParams` will
* still refer to the previous route within these resolve functions. Use `$route.current.params`
* to access the new route parameters, instead.
*
* - `redirectTo` – {(string|function())=} – value to update
* {@link ng.$location $location} path with and trigger route redirection.
*
* If `redirectTo` is a function, it will be called with the following parameters:
*
* - `{Object.<string>}` - route parameters extracted from the current
* `$location.path()` by applying the current route templateUrl.
* - `{string}` - current `$location.path()`
* - `{Object}` - current `$location.search()`
*
* The custom `redirectTo` function is expected to return a string which will be used
* to update `$location.path()` and `$location.search()`.
*
* - `[reloadOnSearch=true]` - {boolean=} - reload route when only $location.search()
* changes.
*
* If the option is set to `false` and url in the browser changes, then
* `$routeUpdate` event is broadcasted on the root scope.
*
* - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
*
* If the option is set to `true`, then the particular route can be matched without being
* case sensitive
*
* @returns {Object} self
*
* @description
* Adds a new route definition to the `$route` service.
*/
this.when = function(path, route) {
routes[path] = extend(
{reloadOnSearch: true},
route,
path && pathRegExp(path, route)
);
// create redirection for trailing slashes
if (path) {
var redirectPath = (path[path.length-1] == '/')
? path.substr(0, path.length-1)
: path +'/';
routes[redirectPath] = extend(
{redirectTo: path},
pathRegExp(redirectPath, route)
);
}
return this;
};
/**
* @param path {string} path
* @param opts {Object} options
* @return {?Object}
*
* @description
* Normalizes the given path, returning a regular expression
* and the original path.
*
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
*/
function pathRegExp(path, opts) {
var insensitive = opts.caseInsensitiveMatch,
ret = {
originalPath: path,
regexp: path
},
keys = ret.keys = [];
path = path
.replace(/([().])/g, '\\$1')
.replace(/(\/)?:(\w+)([\?|\*])?/g, function(_, slash, key, option){
var optional = option === '?' ? option : null;
var star = option === '*' ? option : null;
keys.push({ name: key, optional: !!optional });
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (star && '(.+)?' || '([^/]+)?') + ')'
+ (optional || '');
})
.replace(/([\/$\*])/g, '\\$1');
ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : '');
return ret;
}
/**
* @ngdoc method
* @name ngRoute.$routeProvider#otherwise
* @methodOf ngRoute.$routeProvider
*
* @description
* Sets route definition that will be used on route change when no other route definition
* is matched.
*
* @param {Object} params Mapping information to be assigned to `$route.current`.
* @returns {Object} self
*/
this.otherwise = function(params) {
this.when(null, params);
return this;
};
this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', '$sce',
function( $rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) {
/**
* @ngdoc object
* @name ngRoute.$route
* @requires $location
* @requires $routeParams
*
* @property {Object} current Reference to the current route definition.
* The route definition contains:
*
* - `controller`: The controller constructor as define in route definition.
* - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
* controller instantiation. The `locals` contain
* the resolved values of the `resolve` map. Additionally the `locals` also contain:
*
* - `$scope` - The current route scope.
* - `$template` - The current route template HTML.
*
* @property {Array.<Object>} routes Array of all configured routes.
*
* @description
* `$route` is used for deep-linking URLs to controllers and views (HTML partials).
* It watches `$location.url()` and tries to map the path to an existing route definition.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API.
*
* The `$route` service is typically used in conjunction with the {@link ngRoute.directive:ngView `ngView`}
* directive and the {@link ngRoute.$routeParams `$routeParams`} service.
*
* @example
This example shows how changing the URL hash causes the `$route` to match a route against the
URL, and the `ngView` pulls in the partial.
Note that this example is using {@link ng.directive:script inlined templates}
to get it working on jsfiddle as well.
<example module="ngView" deps="angular-route.js">
<file name="index.html">
<div ng-controller="MainCntl">
Choose:
<a href="Book/Moby">Moby</a> |
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
<a href="Book/Gatsby">Gatsby</a> |
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
<a href="Book/Scarlet">Scarlet Letter</a><br/>
<div ng-view></div>
<hr />
<pre>$location.path() = {{$location.path()}}</pre>
<pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{$route.current.params}}</pre>
<pre>$route.current.scope.name = {{$route.current.scope.name}}</pre>
<pre>$routeParams = {{$routeParams}}</pre>
</div>
</file>
<file name="book.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
</file>
<file name="chapter.html">
controller: {{name}}<br />
Book Id: {{params.bookId}}<br />
Chapter Id: {{params.chapterId}}
</file>
<file name="script.js">
angular.module('ngView', ['ngRoute']).config(function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: BookCntl,
resolve: {
// I will cause a 1 second delay
delay: function($q, $timeout) {
var delay = $q.defer();
$timeout(delay.resolve, 1000);
return delay.promise;
}
}
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
templateUrl: 'chapter.html',
controller: ChapterCntl
});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
}
function BookCntl($scope, $routeParams) {
$scope.name = "BookCntl";
$scope.params = $routeParams;
}
function ChapterCntl($scope, $routeParams) {
$scope.name = "ChapterCntl";
$scope.params = $routeParams;
}
</file>
<file name="scenario.js">
it('should load and compile correct template', function() {
element('a:contains("Moby: Ch1")').click();
var content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
sleep(2); // promises are not part of scenario waiting
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
});
</file>
</example>
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeStart
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted before a route change. At this point the route services starts
* resolving all of the dependencies needed for the route change to occurs.
* Typically this involves fetching the view template as well as any dependencies
* defined in `resolve` route property. Once all of the dependencies are resolved
* `$routeChangeSuccess` is fired.
*
* @param {Route} next Future route information.
* @param {Route} current Current route information.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeSuccess
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted after a route dependencies are resolved.
* {@link ngRoute.directive:ngView ngView} listens for the directive
* to instantiate the controller and render the view.
*
* @param {Object} angularEvent Synthetic event object.
* @param {Route} current Current route information.
* @param {Route|Undefined} previous Previous route information, or undefined if current is first route entered.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeChangeError
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
* Broadcasted if any of the resolve promises are rejected.
*
* @param {Route} current Current route information.
* @param {Route} previous Previous route information.
* @param {Route} rejection Rejection of the promise. Usually the error of the failed promise.
*/
/**
* @ngdoc event
* @name ngRoute.$route#$routeUpdate
* @eventOf ngRoute.$route
* @eventType broadcast on root scope
* @description
*
* The `reloadOnSearch` property has been set to false, and we are reusing the same
* instance of the Controller.
*/
var forceReload = false,
$route = {
routes: routes,
/**
* @ngdoc method
* @name ngRoute.$route#reload
* @methodOf ngRoute.$route
*
* @description
* Causes `$route` service to reload the current route even if
* {@link ng.$location $location} hasn't changed.
*
* As a result of that, {@link ngRoute.directive:ngView ngView}
* creates new scope, reinstantiates the controller.
*/
reload: function() {
forceReload = true;
$rootScope.$evalAsync(updateRoute);
}
};
$rootScope.$on('$locationChangeSuccess', updateRoute);
return $route;
/////////////////////////////////////////////////////
/**
* @param on {string} current url
* @param route {Object} route regexp to match the url against
* @return {?Object}
*
* @description
* Check if the route matches the current url.
*
* Inspired by match in
* visionmedia/express/lib/router/router.js.
*/
function switchRouteMatcher(on, route) {
var keys = route.keys,
params = {};
if (!route.regexp) return null;
var m = route.regexp.exec(on);
if (!m) return null;
for (var i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1];
var val = 'string' == typeof m[i]
? decodeURIComponent(m[i])
: m[i];
if (key && val) {
params[key.name] = val;
}
}
return params;
}
function updateRoute() {
var next = parseRoute(),
last = $route.current;
if (next && last && next.$$route === last.$$route
&& equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) {
last.params = next.params;
copy(last.params, $routeParams);
$rootScope.$broadcast('$routeUpdate', last);
} else if (next || last) {
forceReload = false;
$rootScope.$broadcast('$routeChangeStart', next, last);
$route.current = next;
if (next) {
if (next.redirectTo) {
if (isString(next.redirectTo)) {
$location.path(interpolate(next.redirectTo, next.params)).search(next.params)
.replace();
} else {
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
.replace();
}
}
}
$q.when(next).
then(function() {
if (next) {
var locals = extend({}, next.resolve),
template, templateUrl;
forEach(locals, function(value, key) {
locals[key] = isString(value) ? $injector.get(value) : $injector.invoke(value);
});
if (isDefined(template = next.template)) {
if (isFunction(template)) {
template = template(next.params);
}
} else if (isDefined(templateUrl = next.templateUrl)) {
if (isFunction(templateUrl)) {
templateUrl = templateUrl(next.params);
}
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
if (isDefined(templateUrl)) {
next.loadedTemplateUrl = templateUrl;
template = $http.get(templateUrl, {cache: $templateCache}).
then(function(response) { return response.data; });
}
}
if (isDefined(template)) {
locals['$template'] = template;
}
return $q.all(locals);
}
}).
// after route change
then(function(locals) {
if (next == $route.current) {
if (next) {
next.locals = locals;
copy(next.params, $routeParams);
}
$rootScope.$broadcast('$routeChangeSuccess', next, last);
}
}, function(error) {
if (next == $route.current) {
$rootScope.$broadcast('$routeChangeError', next, last, error);
}
});
}
}
/**
* @returns the current active route, by matching it against the URL
*/
function parseRoute() {
// Match a route
var params, match;
forEach(routes, function(route, path) {
if (!match && (params = switchRouteMatcher($location.path(), route))) {
match = inherit(route, {
params: extend({}, $location.search(), params),
pathParams: params});
match.$$route = route;
}
});
// No route matched; fallback to "otherwise" route
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
}
/**
* @returns interpolation of the redirect path with the parameters
*/
function interpolate(string, params) {
var result = [];
forEach((string||'').split(':'), function(segment, i) {
if (i === 0) {
result.push(segment);
} else {
var segmentMatch = segment.match(/(\w+)(.*)/);
var key = segmentMatch[1];
result.push(params[key]);
result.push(segmentMatch[2] || '');
delete params[key];
}
});
return result.join('');
}
}];
}
ngRouteModule.provider('$routeParams', $RouteParamsProvider);
/**
* @ngdoc object
* @name ngRoute.$routeParams
* @requires $route
*
* @description
* The `$routeParams` service allows you to retrieve the current set of route parameters.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* The route parameters are a combination of {@link ng.$location `$location`}'s
* {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}.
* The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched.
*
* In case of parameter name collision, `path` params take precedence over `search` params.
*
* The service guarantees that the identity of the `$routeParams` object will remain unchanged
* (but its properties will likely change) even when a route change occurs.
*
* Note that the `$routeParams` are only updated *after* a route change completes successfully.
* This means that you cannot rely on `$routeParams` being correct in route resolve functions.
* Instead you can use `$route.current.params` to access the new route's parameters.
*
* @example
* <pre>
* // Given:
* // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby
* // Route: /Chapter/:chapterId/Section/:sectionId
* //
* // Then
* $routeParams ==> {chapterId:1, sectionId:2, search:'moby'}
* </pre>
*/
function $RouteParamsProvider() {
this.$get = function() { return {}; };
}
ngRouteModule.directive('ngView', ngViewFactory);
/**
* @ngdoc directive
* @name ngRoute.directive:ngView
* @restrict ECA
*
* @description
* # Overview
* `ngView` is a directive that complements the {@link ngRoute.$route $route} service by
* including the rendered template of the current route into the main layout (`index.html`) file.
* Every time the current route changes, the included view changes with it according to the
* configuration of the `$route` service.
*
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*
* @animations
* enter - animation is used to bring new content into the browser.
* leave - animation is used to animate existing content away.
*
* The enter and leave animation occur concurrently.
*
* @scope
* @example
<example module="ngViewExample" deps="angular-route.js" animations="true">
<file name="index.html">
<div ng-controller="MainCntl as main">
Choose:
<a href="Book/Moby">Moby</a> |
<a href="Book/Moby/ch/1">Moby: Ch1</a> |
<a href="Book/Gatsby">Gatsby</a> |
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
<a href="Book/Scarlet">Scarlet Letter</a><br/>
<div class="example-animate-container">
<div ng-view class="view-example"></div>
</div>
<hr />
<pre>$location.path() = {{main.$location.path()}}</pre>
<pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre>
<pre>$route.current.params = {{main.$route.current.params}}</pre>
<pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre>
<pre>$routeParams = {{main.$routeParams}}</pre>
</div>
</file>
<file name="book.html">
<div>
controller: {{book.name}}<br />
Book Id: {{book.params.bookId}}<br />
</div>
</file>
<file name="chapter.html">
<div>
controller: {{chapter.name}}<br />
Book Id: {{chapter.params.bookId}}<br />
Chapter Id: {{chapter.params.chapterId}}
</div>
</file>
<file name="animations.css">
.example-animate-container {
position:relative;
background:white;
border:1px solid black;
height:40px;
overflow:hidden;
}
.example-animate-container > div {
padding:10px;
}
.view-example.ng-enter, .view-example.ng-leave {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
display:block;
width:100%;
border-left:1px solid black;
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
padding:10px;
}
.example-animate-container {
position:relative;
height:100px;
}
.view-example.ng-enter {
left:100%;
}
.view-example.ng-enter.ng-enter-active {
left:0;
}
.view-example.ng-leave { }
.view-example.ng-leave.ng-leave-active {
left:-100%;
}
</file>
<file name="script.js">
angular.module('ngViewExample', ['ngRoute', 'ngAnimate'], function($routeProvider, $locationProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: BookCntl,
controllerAs: 'book'
});
$routeProvider.when('/Book/:bookId/ch/:chapterId', {
templateUrl: 'chapter.html',
controller: ChapterCntl,
controllerAs: 'chapter'
});
// configure html5 to get links working on jsfiddle
$locationProvider.html5Mode(true);
});
function MainCntl($route, $routeParams, $location) {
this.$route = $route;
this.$location = $location;
this.$routeParams = $routeParams;
}
function BookCntl($routeParams) {
this.name = "BookCntl";
this.params = $routeParams;
}
function ChapterCntl($routeParams) {
this.name = "ChapterCntl";
this.params = $routeParams;
}
</file>
<file name="scenario.js">
it('should load and compile correct template', function() {
element('a:contains("Moby: Ch1")').click();
var content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element('a:contains("Scarlet")').click();
content = element('.doc-example-live [ng-view]').text();
expect(content).toMatch(/controller\: BookCntl/);
expect(content).toMatch(/Book Id\: Scarlet/);
});
</file>
</example>
*/
/**
* @ngdoc event
* @name ngRoute.directive:ngView#$viewContentLoaded
* @eventOf ngRoute.directive:ngView
* @eventType emit on the current ngView scope
* @description
* Emitted every time the ngView content is reloaded.
*/
ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animate'];
function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animate) {
return {
restrict: 'ECA',
terminal: true,
priority: 1000,
transclude: 'element',
compile: function(element, attr, linker) {
return function(scope, $element, attr) {
var currentScope,
currentElement,
onloadExp = attr.onload || '';
scope.$on('$routeChangeSuccess', update);
update();
function cleanupLastView() {
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
if(currentElement) {
$animate.leave(currentElement);
currentElement = null;
}
}
function update() {
var locals = $route.current && $route.current.locals,
template = locals && locals.$template;
if (template) {
var newScope = scope.$new();
linker(newScope, function(clone) {
cleanupLastView();
clone.html(template);
$animate.enter(clone, null, $element);
var link = $compile(clone.contents()),
current = $route.current;
currentScope = current.scope = newScope;
currentElement = clone;
if (current.controller) {
locals.$scope = currentScope;
var controller = $controller(current.controller, locals);
if (current.controllerAs) {
currentScope[current.controllerAs] = controller;
}
clone.data('$ngControllerController', controller);
clone.contents().data('$ngControllerController', controller);
}
link(currentScope);
currentScope.$emit('$viewContentLoaded');
currentScope.$eval(onloadExp);
// $anchorScroll might listen on event...
$anchorScroll();
});
} else {
cleanupLastView();
}
}
}
}
};
}
})(window, window.angular);
/**
* @license AngularJS v1.1.4
* @license AngularJS v1.2.0-rc.2
* (c) 2010-2012 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {
'use strict';
(function(window, angular, undefined) {'use strict';
var $sanitizeMinErr = angular.$$minErr('$sanitize');
/**
* @ngdoc overview
* @name ngSanitize
* @description
*
* # ngSanitize
*
* The `ngSanitize` module provides functionality to sanitize HTML.
*
* {@installModule sanitize}
*
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
*/
/*
......@@ -48,68 +57,71 @@
<doc:example module="ngSanitize">
<doc:source>
<script>
function Ctrl($scope) {
function Ctrl($scope, $sce) {
$scope.snippet =
'<p style="color:blue">an html\n' +
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
'snippet</p>';
$scope.deliberatelyTrustDangerousSnippet = function() {
return $sce.trustAsHtml($scope.snippet);
};
}
</script>
<div ng-controller="Ctrl">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Filter</td>
<td>Directive</td>
<td>How</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="html-filter">
<td>html filter</td>
<td>
<pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre>
</td>
<td>
<div ng-bind-html="snippet"></div>
</td>
<tr id="bind-html-with-sanitize">
<td>ng-bind-html</td>
<td>Automatically uses $sanitize</td>
<td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind-html="snippet"></div></td>
</tr>
<tr id="bind-html-with-trust">
<td>ng-bind-html</td>
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
<td><pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
</tr>
<tr id="escaped-html">
<td>no filter</td>
<tr id="bind-default">
<td>ng-bind</td>
<td>Automatically escapes</td>
<td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind="snippet"></div></td>
</tr>
<tr id="html-unsafe-filter">
<td>unsafe html filter</td>
<td><pre>&lt;div ng-bind-html-unsafe="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind-html-unsafe="snippet"></div></td>
</tr>
</table>
</div>
</doc:source>
<doc:scenario>
it('should sanitize the html snippet ', function() {
expect(using('#html-filter').element('div').html()).
it('should sanitize the html snippet by default', function() {
expect(using('#bind-html-with-sanitize').element('div').html()).
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
});
it('should inline raw snippet if bound to a trusted value', function() {
expect(using('#bind-html-with-trust').element("div").html()).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should escape snippet without any filter', function() {
expect(using('#escaped-html').element('div').html()).
expect(using('#bind-default').element('div').html()).
toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
"&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
"snippet&lt;/p&gt;");
});
it('should inline raw snippet if filtered as unsafe', function() {
expect(using('#html-unsafe-filter').element("div").html()).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should update', function() {
input('snippet').enter('new <b>text</b>');
expect(using('#html-filter').binding('snippet')).toBe('new <b>text</b>');
expect(using('#escaped-html').element('div').html()).toBe("new &lt;b&gt;text&lt;/b&gt;");
expect(using('#html-unsafe-filter').binding("snippet")).toBe('new <b>text</b>');
input('snippet').enter('new <b onclick="alert(1)">text</b>');
expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new <b>text</b>');
expect(using('#bind-html-with-trust').element('div').html()).toBe('new <b onclick="alert(1)">text</b>');
expect(using('#bind-default').element('div').html()).toBe("new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
});
</doc:scenario>
</doc:example>
......@@ -129,7 +141,7 @@ var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:
BEGING_END_TAGE_REGEXP = /^<\s*\//,
COMMENT_REGEXP = /<!--(.*?)-->/g,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/,
URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/i,
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character)
......@@ -256,7 +268,7 @@ function htmlParser( html, handler ) {
}
if ( html == last ) {
throw "Parse Error: " + html;
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block of html: {0}", html);
}
last = html;
}
......@@ -283,10 +295,10 @@ function htmlParser( html, handler ) {
var attrs = {};
rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) {
rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
var value = doubleQuotedValue
|| singleQoutedValue
|| unqoutedValue
|| singleQuotedValue
|| unquotedValue
|| '';
attrs[name] = decodeEntities(value);
......@@ -400,37 +412,16 @@ function htmlSanitizeWriter(buf){
// define ngSanitize module and register $sanitize service
angular.module('ngSanitize', []).value('$sanitize', $sanitize);
/**
* @ngdoc directive
* @name ngSanitize.directive:ngBindHtml
*
* @description
* Creates a binding that will sanitize the result of evaluating the `expression` with the
* {@link ngSanitize.$sanitize $sanitize} service and innerHTML the result into the current element.
*
* See {@link ngSanitize.$sanitize $sanitize} docs for examples.
*
* @element ANY
* @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate.
*/
angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) {
return function(scope, element, attr) {
element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
scope.$watch(attr.ngBindHtml, function ngBindHtmlWatchAction(value) {
value = $sanitize(value);
element.html(value || '');
});
};
}]);
/**
* @ngdoc filter
* @name ngSanitize.filter:linky
* @function
*
* @description
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
* plain email address links.
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
* plain email address links.
*
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
*
* @param {string} text Input text.
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment