Merge pull request #250 from vrockai/FORMDIS

AngularJS update and directives refactoring
This commit is contained in:
Bill Burke 2014-02-27 16:50:10 -05:00
commit 3e68cc601a
16 changed files with 33357 additions and 19069 deletions

View file

@ -355,8 +355,8 @@ header .navbar {
color: #FFFFFF; color: #FFFFFF;
padding-left: 10px; padding-left: 10px;
} }
.onoffswitch.disabled .onoffswitch-inner .onoffswitch-active, .onoffswitch-checkbox:disabled + .onoffswitch-label .onoffswitch-inner .onoffswitch-active,
.onoffswitch.disabled .onoffswitch-inner .onoffswitch-inactive { .onoffswitch-checkbox:disabled + .onoffswitch-label .onoffswitch-inner .onoffswitch-inactive {
background-image: none; background-image: none;
background-color: #e5e5e5; background-color: #e5e5e5;
color: #9d9fa1; color: #9d9fa1;

View file

@ -21,6 +21,7 @@
<script src="lib/angular/angular.js"></script> <script src="lib/angular/angular.js"></script>
<script src="lib/angular/angular-resource.js"></script> <script src="lib/angular/angular-resource.js"></script>
<script src="lib/angular/angular-route.js"></script>
<script src="lib/angular/ui-bootstrap-tpls-0.4.0.js"></script> <script src="lib/angular/ui-bootstrap-tpls-0.4.0.js"></script>
<script src="lib/jquery/jquery.idletimer.js" type="text/javascript"></script> <script src="lib/jquery/jquery.idletimer.js" type="text/javascript"></script>

View file

@ -640,35 +640,36 @@ module.directive('collapsed', function() {
module.directive('onoffswitch', function() { module.directive('onoffswitch', function() {
return { return {
restrict: "EA", restrict: "EA",
require: 'ngModel',
replace: true, replace: true,
scope: { scope: {
name: '@',
id: '@',
ngModel: '=', ngModel: '=',
ngDisabled: '=', kcOnText: '@onText',
ngBind: '=', kcOffText: '@offText'
name: '=',
id: '=',
onText: '@onText',
offText: '@offText'
}, },
// TODO - The same code acts differently when put into the templateURL. Find why and move the code there.
//templateUrl: "templates/kc-switch.html",
template: "<span><div class='onoffswitch' tabindex='0'><input type='checkbox' ng-model='ngModel' class='onoffswitch-checkbox' name='{{name}}' id='{{id}}'><label for='{{id}}' class='onoffswitch-label'><span class='onoffswitch-inner'><span class='onoffswitch-active'>{{kcOnText}}</span><span class='onoffswitch-inactive'>{{kcOffText}}</span></span><span class='onoffswitch-switch'></span></label></div></span>",
compile: function(element, attrs) { compile: function(element, attrs) {
/*
We don't want to propagate basic attributes to the root element of directive. Id should be passed to the
input element only to achieve proper label binding (and validity).
*/
element.removeAttr('name');
element.removeAttr('id');
if (!attrs.onText) { attrs.onText = "ON"; } if (!attrs.onText) { attrs.onText = "ON"; }
if (!attrs.offText) { attrs.offText = "OFF"; } if (!attrs.offText) { attrs.offText = "OFF"; }
if (!attrs.ngDisabled) { attrs.ngDisabled = false; }
var html = "<span><div class=\"onoffswitch\" data-ng-class=\"{disabled: ngDisabled}\" tabindex=\"0\" onkeydown=\"var code = event.keyCode || event.which; if (code === 32 || code === 13) { event.stopImmediatePropagation(); event.preventDefault(); $(event.target).find('input').click();}\">" + element.bind('keydown', function(e){
"<input type=\"checkbox\" data-ng-model=\"ngModel\" ng-disabled=\"ngDisabled\"" + var code = e.keyCode || e.which;
" class=\"onoffswitch-checkbox\" name=\"" + attrs.name + "\" id=\"" + attrs.id + "\">" + if (code === 32 || code === 13) {
"<label for=\"" + attrs.id + "\" class=\"onoffswitch-label\">" + e.stopImmediatePropagation();
"<span class=\"onoffswitch-inner\">" + e.preventDefault();
"<span class=\"onoffswitch-active\">{{onText}}</span>" + $(e.target).find('input').click();
"<span class=\"onoffswitch-inactive\">{{offText}}</span>" + }
"</span>" + });
"<span class=\"onoffswitch-switch\"></span>" +
"</label>" +
"</div></span>";
element.replaceWith($(html));
} }
} }
}); });
@ -790,7 +791,7 @@ module.directive('kcDelete', function ($compile, Notifications) {
}); });
module.directive('kcSelect', function ($compile, Notifications) { module.directive('kcDropdown', function ($compile, Notifications) {
return { return {
scope: { scope: {
kcOptions: '=', kcOptions: '=',

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
var module = angular.module('keycloak.services', [ 'ngResource' ]); var module = angular.module('keycloak.services', [ 'ngResource', 'ngRoute' ]);
module.service('Dialog', function($dialog) { module.service('Dialog', function($dialog) {
var dialog = {}; var dialog = {};

View file

@ -1,14 +1,25 @@
/** /**
* @license AngularJS v1.0.7 * @license AngularJS v1.2.13
* (c) 2010-2012 Google, Inc. http://angularjs.org * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
(function(window, angular, undefined) { (function(window, angular, undefined) {'use strict';
'use strict';
/** /**
* @ngdoc overview * @ngdoc overview
* @name ngCookies * @name ngCookies
* @description
*
* # ngCookies
*
* The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies.
*
* {@installModule cookies}
*
* <div doc-module-components="ngCookies"></div>
*
* See {@link ngCookies.$cookies `$cookies`} and
* {@link ngCookies.$cookieStore `$cookieStore`} for usage.
*/ */
@ -24,6 +35,8 @@ angular.module('ngCookies', ['ng']).
* Only a simple Object is exposed and by adding or removing properties to/from * Only a simple Object is exposed and by adding or removing properties to/from
* this object, new cookies are created/deleted at the end of current $eval. * this object, new cookies are created/deleted at the end of current $eval.
* *
* Requires the {@link ngCookies `ngCookies`} module to be installed.
*
* @example * @example
<doc:example> <doc:example>
<doc:source> <doc:source>
@ -68,7 +81,8 @@ angular.module('ngCookies', ['ng']).
/** /**
* Pushes all the cookies from the service to the browser and verifies if all cookies were stored. * Pushes all the cookies from the service to the browser and verifies if all cookies were
* stored.
*/ */
function push() { function push() {
var name, var name,
@ -128,6 +142,9 @@ angular.module('ngCookies', ['ng']).
* Provides a key-value (string-object) storage, that is backed by session cookies. * Provides a key-value (string-object) storage, that is backed by session cookies.
* Objects put or retrieved from this storage are automatically serialized or * Objects put or retrieved from this storage are automatically serialized or
* deserialized by angular's toJson/fromJson. * deserialized by angular's toJson/fromJson.
*
* Requires the {@link ngCookies `ngCookies`} module to be installed.
*
* @example * @example
*/ */
factory('$cookieStore', ['$cookies', function($cookies) { factory('$cookieStore', ['$cookies', function($cookies) {

View file

@ -1,10 +1,84 @@
/** /**
* @license AngularJS v1.0.7 * @license AngularJS v1.2.13
* (c) 2010-2012 Google, Inc. http://angularjs.org * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
( (function() {'use strict';
/**
* @description
*
* This object provides a utility for producing rich Error messages within
* Angular. It can be called as follows:
*
* var exampleMinErr = minErr('example');
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
*
* The above creates an instance of minErr in the example namespace. The
* resulting error will have a namespaced error code of example.one. The
* resulting error will replace {0} with the value of foo, and {1} with the
* value of bar. The object is not restricted in the number of arguments it can
* take.
*
* If fewer arguments are specified than necessary for interpolation, the extra
* interpolation markers will be preserved in the final string.
*
* Since data will be parsed statically during a build step, some restrictions
* are applied with respect to how minErr instances are created and called.
* Instances should have names of the form namespaceMinErr for a minErr created
* using minErr('namespace') . Error codes, namespaces and template strings
* should all be static strings, not variables or general expressions.
*
* @param {string} module The namespace to use for the new minErr instance.
* @returns {function(string, string, ...): Error} instance
*/
function minErr(module) {
return function () {
var code = arguments[0],
prefix = '[' + (module ? module + ':' : '') + code + '] ',
template = arguments[1],
templateArgs = arguments,
stringify = function (obj) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (typeof obj === 'undefined') {
return 'undefined';
} else if (typeof obj !== 'string') {
return JSON.stringify(obj);
}
return obj;
},
message, i;
message = prefix + template.replace(/\{\d+\}/g, function (match) {
var index = +match.slice(1, -1), arg;
if (index + 2 < templateArgs.length) {
arg = templateArgs[index + 2];
if (typeof arg === 'function') {
return arg.toString().replace(/ ?\{[\s\S]*$/, '');
} else if (typeof arg === 'undefined') {
return 'undefined';
} else if (typeof arg !== 'string') {
return toJson(arg);
}
return arg;
}
return match;
});
message = message + '\nhttp://errors.angularjs.org/1.2.13/' +
(module ? module + '/' : '') + code;
for (i = 2; i < arguments.length; i++) {
message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' +
encodeURIComponent(stringify(arguments[i]));
}
return new Error(message);
};
}
/** /**
* @ngdoc interface * @ngdoc interface
@ -16,11 +90,19 @@
function setupModuleLoader(window) { function setupModuleLoader(window) {
var $injectorMinErr = minErr('$injector');
var ngMinErr = minErr('ng');
function ensure(obj, name, factory) { function ensure(obj, name, factory) {
return obj[name] || (obj[name] = factory()); return obj[name] || (obj[name] = factory());
} }
return ensure(ensure(window, 'angular', Object), 'module', function() { var angular = ensure(window, 'angular', Object);
// We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap
angular.$$minErr = angular.$$minErr || minErr;
return ensure(angular, 'module', function() {
/** @type {Object.<string, angular.Module>} */ /** @type {Object.<string, angular.Module>} */
var modules = {}; var modules = {};
@ -29,15 +111,19 @@ function setupModuleLoader(window) {
* @name angular.module * @name angular.module
* @description * @description
* *
* The `angular.module` is a global place for creating and registering Angular modules. All * The `angular.module` is a global place for creating, registering and retrieving Angular
* modules (angular core or 3rd party) that should be available to an application must be * modules.
* All modules (angular core or 3rd party) that should be available to an application must be
* registered using this mechanism. * registered using this mechanism.
* *
* When passed two or more arguments, a new module is created. If passed only one argument, an
* existing module (the name passed as the first argument to `module`) is retrieved.
*
* *
* # Module * # Module
* *
* A module is a collocation of services, directives, filters, and configuration information. Module * A module is a collection of services, directives, filters, and configuration information.
* is used to configure the {@link AUTO.$injector $injector}. * `angular.module` is used to configure the {@link AUTO.$injector $injector}.
* *
* <pre> * <pre>
* // Create a new module * // Create a new module
@ -48,7 +134,6 @@ function setupModuleLoader(window) {
* *
* // configure existing services inside initialization blocks. * // configure existing services inside initialization blocks.
* myModule.config(function($locationProvider) { * myModule.config(function($locationProvider) {
'use strict';
* // Configure existing providers * // Configure existing providers
* $locationProvider.hashPrefix('!'); * $locationProvider.hashPrefix('!');
* }); * });
@ -65,19 +150,28 @@ function setupModuleLoader(window) {
* {@link angular.bootstrap} to simplify this process for you. * {@link angular.bootstrap} to simplify this process for you.
* *
* @param {!string} name The name of the module to create or retrieve. * @param {!string} name The name of the module to create or retrieve.
* @param {Array.<string>=} requires If specified then new module is being created. If unspecified then the * @param {Array.<string>=} requires If specified then new module is being created. If
* the module is being retrieved for further configuration. * unspecified then the the module is being retrieved for further configuration.
* @param {Function} configFn Optional configuration function for the module. Same as * @param {Function} configFn Optional configuration function for the module. Same as
* {@link angular.Module#config Module#config()}. * {@link angular.Module#methods_config Module#config()}.
* @returns {module} new module with the {@link angular.Module} api. * @returns {module} new module with the {@link angular.Module} api.
*/ */
return function module(name, requires, configFn) { return function module(name, requires, configFn) {
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
}
};
assertNotHasOwnProperty(name, 'module');
if (requires && modules.hasOwnProperty(name)) { if (requires && modules.hasOwnProperty(name)) {
modules[name] = null; modules[name] = null;
} }
return ensure(modules, name, function() { return ensure(modules, name, function() {
if (!requires) { if (!requires) {
throw Error('No module: ' + name); throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
"the module name or forgot to load it. If registering a module ensure that you " +
"specify the dependencies as the second argument.", name);
} }
/** @type {!Array.<Array.<*>>} */ /** @type {!Array.<Array.<*>>} */
@ -100,7 +194,8 @@ function setupModuleLoader(window) {
* @propertyOf angular.Module * @propertyOf angular.Module
* @returns {Array.<string>} List of module names which must be loaded before this module. * @returns {Array.<string>} List of module names which must be loaded before this module.
* @description * @description
* Holds the list of modules which the injector will load before the current module is loaded. * Holds the list of modules which the injector will load before the current module is
* loaded.
*/ */
requires: requires, requires: requires,
@ -119,7 +214,8 @@ function setupModuleLoader(window) {
* @name angular.Module#provider * @name angular.Module#provider
* @methodOf angular.Module * @methodOf angular.Module
* @param {string} name service name * @param {string} name service name
* @param {Function} providerType Construction function for creating new instance of the service. * @param {Function} providerType Construction function for creating new instance of the
* service.
* @description * @description
* See {@link AUTO.$provide#provider $provide.provider()}. * See {@link AUTO.$provide#provider $provide.provider()}.
*/ */
@ -170,6 +266,40 @@ function setupModuleLoader(window) {
*/ */
constant: invokeLater('$provide', 'constant', 'unshift'), constant: invokeLater('$provide', 'constant', 'unshift'),
/**
* @ngdoc method
* @name angular.Module#animation
* @methodOf angular.Module
* @param {string} name animation name
* @param {Function} animationFactory Factory function for creating new instance of an
* animation.
* @description
*
* **NOTE**: animations take effect only if the **ngAnimate** module is loaded.
*
*
* Defines an animation hook that can be later used with
* {@link ngAnimate.$animate $animate} service and directives that use this service.
*
* <pre>
* module.animation('.animation-name', function($inject1, $inject2) {
* return {
* eventName : function(element, done) {
* //code to run the animation
* //once complete, then run done()
* return function cancellationFunction(element) {
* //code to cancel the animation
* }
* }
* }
* })
* </pre>
*
* See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and
* {@link ngAnimate ngAnimate module} for more information.
*/
animation: invokeLater('$animateProvider', 'register'),
/** /**
* @ngdoc method * @ngdoc method
* @name angular.Module#filter * @name angular.Module#filter
@ -185,7 +315,8 @@ function setupModuleLoader(window) {
* @ngdoc method * @ngdoc method
* @name angular.Module#controller * @name angular.Module#controller
* @methodOf angular.Module * @methodOf angular.Module
* @param {string} name Controller name. * @param {string|Object} name Controller name, or an object map of controllers where the
* keys are the names and the values are the constructors.
* @param {Function} constructor Controller constructor function. * @param {Function} constructor Controller constructor function.
* @description * @description
* See {@link ng.$controllerProvider#register $controllerProvider.register()}. * See {@link ng.$controllerProvider#register $controllerProvider.register()}.
@ -196,11 +327,12 @@ function setupModuleLoader(window) {
* @ngdoc method * @ngdoc method
* @name angular.Module#directive * @name angular.Module#directive
* @methodOf angular.Module * @methodOf angular.Module
* @param {string} name directive name * @param {string|Object} name Directive name, or an object map of directives where the
* keys are the names and the values are the factories.
* @param {Function} directiveFactory Factory function for creating new instance of * @param {Function} directiveFactory Factory function for creating new instance of
* directives. * directives.
* @description * @description
* See {@link ng.$compileProvider#directive $compileProvider.directive()}. * See {@link ng.$compileProvider#methods_directive $compileProvider.directive()}.
*/ */
directive: invokeLater('$compileProvider', 'directive'), directive: invokeLater('$compileProvider', 'directive'),
@ -247,7 +379,7 @@ function setupModuleLoader(window) {
return function() { return function() {
invokeQueue[insertMethod || 'push']([provider, method, arguments]); invokeQueue[insertMethod || 'push']([provider, method, arguments]);
return moduleInstance; return moduleInstance;
} };
} }
}); });
}; };
@ -255,7 +387,8 @@ function setupModuleLoader(window) {
} }
)(window); setupModuleLoader(window);
})(window);
/** /**
* Closure compiler type information * Closure compiler type information

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,68 @@
/** /**
* @license AngularJS v1.0.7 * @license AngularJS v1.2.13
* (c) 2010-2012 Google, Inc. http://angularjs.org * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
(function(window, angular, undefined) { (function(window, angular, undefined) {'use strict';
'use strict';
var $resourceMinErr = angular.$$minErr('$resource');
// Helper functions and regex to lookup a dotted path on an object
// stopping at undefined/null. The path must be composed of ASCII
// identifiers (just like $parse)
var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;
function isValidDottedPath(path) {
return (path != null && path !== '' && path !== 'hasOwnProperty' &&
MEMBER_NAME_REGEX.test('.' + path));
}
function lookupDottedPath(obj, path) {
if (!isValidDottedPath(path)) {
throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path);
}
var keys = path.split('.');
for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) {
var key = keys[i];
obj = (obj !== null) ? obj[key] : undefined;
}
return obj;
}
/**
* Create a shallow copy of an object and clear other fields from the destination
*/
function shallowClearAndCopy(src, dst) {
dst = dst || {};
angular.forEach(dst, function(value, key){
delete dst[key];
});
for (var key in src) {
if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
dst[key] = src[key];
}
}
return dst;
}
/** /**
* @ngdoc overview * @ngdoc overview
* @name ngResource * @name ngResource
* @description * @description
*
* # ngResource
*
* The `ngResource` module provides interaction support with RESTful services
* via the $resource service.
*
* {@installModule resource}
*
* <div doc-module-components="ngResource"></div>
*
* See {@link ngResource.$resource `$resource`} for usage.
*/ */
/** /**
@ -24,24 +77,22 @@
* The returned resource object has action methods which provide high-level behaviors without * 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. * the need to interact with the low level {@link ng.$http $http} service.
* *
* # Installation * Requires the {@link ngResource `ngResource`} module to be installed.
* To use $resource make sure you have included the `angular-resource.js` that comes in Angular
* package. You can also find this file on Google CDN, bower as well as at
* {@link http://code.angularjs.org/ code.angularjs.org}.
* *
* Finally load the module in your application: * @param {string} url A parametrized URL template with parameters prefixed by `:` as in
*
* angular.module('app', ['ngResource']);
*
* and you are ready to get started!
*
* @param {string} url A parameterized URL template with parameters prefixed by `:` as in
* `/user/:username`. If you are using a URL with a port number (e.g. * `/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 * `http://example.com:8080/api`), it will be respected.
* number, like this: `$resource('http://example.com\\:8080/api')`. *
* 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 * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
* `actions` methods. * `actions` methods. If any of the parameter value is a function, it will be executed every time
* when a param value needs to be obtained for a request (unless the param was overridden).
* *
* Each key value in the parameter object is first bound to url template if present and then any * Each key value in the parameter object is first bound to url template if present and then any
* excess keys are appended to the url search query after the `?`. * excess keys are appended to the url search query after the `?`.
@ -53,21 +104,48 @@
* the data object (useful for non-GET operations). * the data object (useful for non-GET operations).
* *
* @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the
* default set of resource actions. The declaration should be created in the following format: * default set of resource actions. The declaration should be created in the format of {@link
* ng.$http#usage_parameters $http.config}:
* *
* {action1: {method:?, params:?, isArray:?}, * {action1: {method:?, params:?, isArray:?, headers:?, ...},
* action2: {method:?, params:?, isArray:?}, * action2: {method:?, params:?, isArray:?, headers:?, ...},
* ...} * ...}
* *
* Where: * Where:
* *
* - `action` {string} The name of action. This name becomes the name of the method on your * - **`action`** {string} The name of action. This name becomes the name of the method on
* resource object. * your resource object.
* - `method` {string} HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, * - **`method`** {string} HTTP request method. Valid methods are: `GET`, `POST`, `PUT`,
* and `JSONP` * `DELETE`, and `JSONP`.
* - `params` {object=} Optional set of pre-bound parameters for this action. * - **`params`** {Object=} Optional set of pre-bound parameters for this action. If any of
* - isArray {boolean=} If true then the returned object for this action is an array, see * the parameter value is a function, it will be executed every time when a param value needs to
* `returns` section. * be obtained for a request (unless the param was overridden).
* - **`url`** {string} action specific `url` override. The url templating is supported just
* like for the resource-level urls.
* - **`isArray`** {boolean=} If true then the returned object for this action is an array,
* see `returns` section.
* - **`transformRequest`**
* `{function(data, headersGetter)|Array.<function(data, headersGetter)>}`
* transform function or an array of such functions. The transform function takes the http
* request body and headers and returns its transformed (typically serialized) version.
* - **`transformResponse`**
* `{function(data, headersGetter)|Array.<function(data, headersGetter)>}`
* transform function or an array of such functions. The transform function takes the http
* response body and headers and returns its transformed (typically deserialized) version.
* - **`cache`** `{boolean|Cache}` If true, a default $http cache will be used to cache the
* GET request, otherwise if a cache instance built with
* {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
* caching.
* - **`timeout`** `{number|Promise}` timeout in milliseconds, or {@link ng.$q promise} that
* should abort the request when resolved.
* - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
* XHR object. See {@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 * @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: * optionally extended with custom `actions`. The default set contains these actions:
@ -97,7 +175,7 @@
* usually the resource is assigned to a model which is then rendered by the view. Having an empty * usually the resource is assigned to a model which is then rendered by the view. Having an empty
* object results in no rendering, once the data arrives from the server then the object is * object results in no rendering, once the data arrives from the server then the object is
* populated with the data and the view automatically re-renders itself showing the new data. This * populated with the data and the view automatically re-renders itself showing the new data. This
* means that in most case one never has to write a callback function for the action methods. * means that in most cases one never has to write a callback function for the action methods.
* *
* The action methods on the class object or instance object can be invoked with the following * The action methods on the class object or instance object can be invoked with the following
* parameters: * parameters:
@ -106,6 +184,28 @@
* - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
* - non-GET instance actions: `instance.$action([parameters], [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.
*
* Class actions return empty instance (with additional properties below).
* Instance actions return promise of the action.
*
* The Resource instances and collection have these additional properties:
*
* - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
* instance or collection.
*
* On success, the promise is resolved with the same resource instance or collection object,
* updated with data from server. This makes it easy to use in
* {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view
* rendering until the resource(s) are loaded.
*
* On failure, the promise is resolved with the {@link ng.$http http response} object, without
* the `resource` property.
*
* - `$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 * @example
* *
@ -142,14 +242,15 @@
newCard.name = "Mike Smith"; newCard.name = "Mike Smith";
newCard.$save(); newCard.$save();
// POST: /user/123/card {number:'0123', name:'Mike Smith'} // POST: /user/123/card {number:'0123', name:'Mike Smith'}
// server returns: {id:789, number:'01234', name: 'Mike Smith'}; // server returns: {id:789, number:'0123', name: 'Mike Smith'};
expect(newCard.id).toEqual(789); expect(newCard.id).toEqual(789);
* </pre> * </pre>
* *
* The object returned from this function execution is a resource "class" which has "static" method * The object returned from this function execution is a resource "class" which has "static" method
* for each action in the definition. * for each action in the definition.
* *
* Calling these methods invoke `$http` on the `url` template with the given `method` and `params`. * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and
* `headers`.
* When the data is returned from the server then the object is an instance of the resource type and * When the data is returned from the server then the object is an instance of the resource type and
* all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
* operations (create, read, update, delete) on server-side data. * operations (create, read, update, delete) on server-side data.
@ -162,7 +263,7 @@
}); });
</pre> </pre>
* *
* It's worth noting that the success callback for `get`, `query` and other method gets passed * It's worth noting that the success callback for `get`, `query` and other methods gets passed
* in the response that came from the server as well as $http header getter function, so one * in the response that came from the server as well as $http header getter function, so one
* could rewrite the above example and get access to http headers as: * could rewrite the above example and get access to http headers as:
* *
@ -177,56 +278,38 @@
}); });
</pre> </pre>
* # Buzz client * # Creating a custom 'PUT' request
* In this example we create a custom method on our resource to make a PUT request
Let's look at what a buzz client created with the `$resource` service looks like: * <pre>
<doc:example> * var app = angular.module('app', ['ngResource', 'ngRoute']);
<doc:source jsfiddle="false"> *
<script> * // Some APIs expect a PUT request in the format URL/object/ID
function BuzzController($resource) { * // Here we are creating an 'update' method
this.userId = 'googlebuzz'; * app.factory('Notes', ['$resource', function($resource) {
this.Activity = $resource( * return $resource('/notes/:id', null,
'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments', * {
{alt:'json', callback:'JSON_CALLBACK'}, * 'update': { method:'PUT' }
{get:{method:'JSONP', params:{visibility:'@self'}}, replies: {method:'JSONP', params:{visibility:'@self', comments:'@comments'}}} * });
); * }]);
} *
* // In our controller we get the ID from the URL using ngRoute and $routeParams
BuzzController.prototype = { * // We pass in $routeParams and our Notes factory along with $scope
fetch: function() { * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes',
this.activities = this.Activity.get({userId:this.userId}); function($scope, $routeParams, Notes) {
}, * // First get a note object from the factory
expandReplies: function(activity) { * var note = Notes.get({ id:$routeParams.id });
activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id}); * $id = note.id;
} *
}; * // Now call update passing in the ID first then the object you are updating
BuzzController.$inject = ['$resource']; * Notes.update({ id:$id }, note);
</script> *
* // This will PUT /notes/ID with the note object in the request payload
<div ng-controller="BuzzController"> * }]);
<input ng-model="userId"/> * </pre>
<button ng-click="fetch()">fetch</button>
<hr/>
<div ng-repeat="item in activities.data.items">
<h1 style="font-size: 15px;">
<img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
<a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
<a href ng-click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
</h1>
{{item.object.content | html}}
<div ng-repeat="reply in item.replies.data.items" style="margin-left: 20px;">
<img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
<a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}}
</div>
</div>
</div>
</doc:source>
<doc:scenario>
</doc:scenario>
</doc:example>
*/ */
angular.module('ngResource', ['ng']). angular.module('ngResource', ['ng']).
factory('$resource', ['$http', '$parse', function($http, $parse) { factory('$resource', ['$http', '$q', function($http, $q) {
var DEFAULT_ACTIONS = { var DEFAULT_ACTIONS = {
'get': {method:'GET'}, 'get': {method:'GET'},
'save': {method:'POST'}, 'save': {method:'POST'},
@ -238,10 +321,7 @@ angular.module('ngResource', ['ng']).
forEach = angular.forEach, forEach = angular.forEach,
extend = angular.extend, extend = angular.extend,
copy = angular.copy, copy = angular.copy,
isFunction = angular.isFunction, isFunction = angular.isFunction;
getter = function(obj, path) {
return $parse(path)(obj);
};
/** /**
* We need our custom method because encodeURIComponent is too aggressive and doesn't follow * We need our custom method because encodeURIComponent is too aggressive and doesn't follow
@ -263,9 +343,9 @@ angular.module('ngResource', ['ng']).
/** /**
* This method is intended for encoding *key* or *value* parts of query component. We need a custom * This method is intended for encoding *key* or *value* parts of query component. We need a
* method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't
* encoded per http://tools.ietf.org/html/rfc3986: * have to be encoded per http://tools.ietf.org/html/rfc3986:
* query = *( pchar / "/" / "?" ) * query = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
@ -283,32 +363,40 @@ angular.module('ngResource', ['ng']).
} }
function Route(template, defaults) { function Route(template, defaults) {
this.template = template = template + '#'; this.template = template;
this.defaults = defaults || {}; this.defaults = defaults || {};
var urlParams = this.urlParams = {}; this.urlParams = {};
forEach(template.split(/\W/), function(param){
if (param && (new RegExp("(^|[^\\\\]):" + param + "\\W").test(template))) {
urlParams[param] = true;
}
});
this.template = template.replace(/\\:/g, ':');
} }
Route.prototype = { Route.prototype = {
url: function(params) { setUrlParams: function(config, params, actionUrl) {
var self = this, var self = this,
url = this.template, url = actionUrl || self.template,
val, val,
encodedVal; encodedVal;
var urlParams = self.urlParams = {};
forEach(url.split(/\W/), function(param){
if (param === 'hasOwnProperty') {
throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name.");
}
if (!(new RegExp("^\\d+$").test(param)) && param &&
(new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
urlParams[param] = true;
}
});
url = url.replace(/\\:/g, ':');
params = params || {}; params = params || {};
forEach(this.urlParams, function(_, urlParam){ forEach(self.urlParams, function(_, urlParam){
val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
if (angular.isDefined(val) && val !== null) { if (angular.isDefined(val) && val !== null) {
encodedVal = encodeUriSegment(val); encodedVal = encodeUriSegment(val);
url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1"); url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) {
return encodedVal + p1;
});
} else { } else {
url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W)", "g"), function(match, url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match,
leadingSlashes, tail) { leadingSlashes, tail) {
if (tail.charAt(0) == '/') { if (tail.charAt(0) == '/') {
return tail; return tail;
@ -318,21 +406,28 @@ angular.module('ngResource', ['ng']).
}); });
} }
}); });
url = url.replace(/\/?#$/, '');
var query = []; // 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){ forEach(params, function(value, key){
if (!self.urlParams[key]) { if (!self.urlParams[key]) {
query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value)); config.params = config.params || {};
config.params[key] = value;
} }
}); });
query.sort();
url = url.replace(/\/*$/, '');
return url + (query.length ? '?' + query.join('&') : '');
} }
}; };
function ResourceFactory(url, paramDefaults, actions) { function resourceFactory(url, paramDefaults, actions) {
var route = new Route(url); var route = new Route(url);
actions = extend({}, DEFAULT_ACTIONS, actions); actions = extend({}, DEFAULT_ACTIONS, actions);
@ -341,23 +436,28 @@ angular.module('ngResource', ['ng']).
var ids = {}; var ids = {};
actionParams = extend({}, paramDefaults, actionParams); actionParams = extend({}, paramDefaults, actionParams);
forEach(actionParams, function(value, key){ forEach(actionParams, function(value, key){
ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; if (isFunction(value)) { value = value(); }
ids[key] = value && value.charAt && value.charAt(0) == '@' ?
lookupDottedPath(data, value.substr(1)) : value;
}); });
return ids; return ids;
} }
function defaultResponseInterceptor(response) {
return response.resource;
}
function Resource(value){ function Resource(value){
copy(value || {}, this); shallowClearAndCopy(value || {}, this);
} }
forEach(actions, function(action, name) { forEach(actions, function(action, name) {
action.method = angular.uppercase(action.method); var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);
var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
Resource[name] = function(a1, a2, a3, a4) { Resource[name] = function(a1, a2, a3, a4) {
var params = {}; var params = {}, data, success, error;
var data;
var success = noop; /* jshint -W086 */ /* (purposefully fall through case statements) */
var error = null;
switch(arguments.length) { switch(arguments.length) {
case 4: case 4:
error = a4; error = a4;
@ -388,69 +488,108 @@ angular.module('ngResource', ['ng']).
break; break;
case 0: break; case 0: break;
default: default:
throw "Expected between 0-4 arguments [params, data, success, error], got " + throw $resourceMinErr('badargs',
arguments.length + " arguments."; "Expected up to 4 arguments [params, data, success, error], got {0} arguments",
arguments.length);
} }
/* jshint +W086 */ /* (purposefully fall through case statements) */
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); var isInstanceCall = this instanceof Resource;
$http({ var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
method: action.method, var httpConfig = {};
url: route.url(extend({}, extractParams(data, action.params || {}), params)), var responseInterceptor = action.interceptor && action.interceptor.response ||
data: data defaultResponseInterceptor;
}).then(function(response) { var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
var data = response.data; undefined;
forEach(action, function(value, key) {
if (key != 'params' && key != 'isArray' && key != 'interceptor') {
httpConfig[key] = copy(value);
}
});
if (hasBody) httpConfig.data = data;
route.setUrlParams(httpConfig,
extend({}, extractParams(data, action.params || {}), params),
action.url);
var promise = $http(httpConfig).then(function(response) {
var data = response.data,
promise = value.$promise;
if (data) { if (data) {
// Need to convert action.isArray to boolean in case it is undefined
// jshint -W018
if (angular.isArray(data) !== (!!action.isArray)) {
throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' +
'response to contain an {0} but got an {1}',
action.isArray?'array':'object', angular.isArray(data)?'array':'object');
}
// jshint +W018
if (action.isArray) { if (action.isArray) {
value.length = 0; value.length = 0;
forEach(data, function(item) { forEach(data, function(item) {
value.push(new Resource(item)); value.push(new Resource(item));
}); });
} else { } else {
copy(data, value); shallowClearAndCopy(data, value);
value.$promise = promise;
} }
} }
value.$resolved = true;
response.resource = value;
return response;
}, function(response) {
value.$resolved = true;
(error||noop)(response);
return $q.reject(response);
});
promise = promise.then(
function(response) {
var value = responseInterceptor(response);
(success||noop)(value, response.headers); (success||noop)(value, response.headers);
}, error); return value;
},
responseErrorInterceptor);
if (!isInstanceCall) {
// we are creating instance / collection
// - set the initial promise
// - return the instance / collection
value.$promise = promise;
value.$resolved = false;
return value; return value;
}
// instance call
return promise;
}; };
Resource.prototype['$' + name] = function(a1, a2, a3) { Resource.prototype['$' + name] = function(params, success, error) {
var params = extractParams(this), if (isFunction(params)) {
success = noop, error = success; success = params; params = {};
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; var result = Resource[name].call(this, params, this, success, error);
default: return result.$promise || result;
throw "Expected between 1-3 arguments [params, success, error], got " +
arguments.length + " arguments.";
}
var data = hasBody ? this : undefined;
Resource[name].call(this, params, data, success, error);
}; };
}); });
Resource.bind = function(additionalParamDefaults){ Resource.bind = function(additionalParamDefaults){
return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
}; };
return Resource; return Resource;
} }
return ResourceFactory; return resourceFactory;
}]); }]);

View file

@ -0,0 +1,921 @@
/**
* @license AngularJS v1.2.13
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/**
* @ngdoc overview
* @name ngRoute
* @description
*
* # ngRoute
*
* The `ngRoute` module provides routing and deeplinking services and directives for angular apps.
*
* ## Example
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
*
* {@installModule route}
*
* <div doc-module-components="ngRoute"></div>
*/
/* global -ngRouteModule */
var ngRouteModule = angular.module('ngRoute', ['ng']).
provider('$route', $RouteProvider);
/**
* @ngdoc object
* @name ngRoute.$routeProvider
* @function
*
* @description
*
* Used for configuring routes.
*
* ## Example
* See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`.
*
* ## Dependencies
* Requires the {@link ngRoute `ngRoute`} module to be installed.
*/
function $RouteProvider(){
function inherit(parent, extra) {
return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra);
}
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: e.g. `: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:
* e.g.`: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: e.g.`: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, the router
* will wait for them all to be resolved or one to be rejected before the controller is
* instantiated.
* If all the promises are resolved successfully, the values of the resolved promises are
* injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
* fired. If any of the promises are rejected the
* {@link ngRoute.$route#$routeChangeError $routeChangeError} 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()`
* or `$location.hash()` 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] = angular.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] = angular.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 || '')
+ ')'
+ (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="ngViewExample" 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('ngViewExample', ['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="protractorTest.js">
it('should load and compile correct template', function() {
element(by.linkText('Moby: Ch1')).click();
var content = element(by.css('.doc-example-live [ng-view]')).getText();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element(by.partialLinkText('Scarlet')).click();
content = element(by.css('.doc-example-live [ng-view]')).getText();
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 occur.
* 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 {Object} angularEvent Synthetic event object.
* @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 {Object} angularEvent Synthetic event object
* @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
&& angular.equals(next.pathParams, last.pathParams)
&& !next.reloadOnSearch && !forceReload) {
last.params = next.params;
angular.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 (angular.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 = angular.extend({}, next.resolve),
template, templateUrl;
angular.forEach(locals, function(value, key) {
locals[key] = angular.isString(value) ?
$injector.get(value) : $injector.invoke(value);
});
if (angular.isDefined(template = next.template)) {
if (angular.isFunction(template)) {
template = template(next.params);
}
} else if (angular.isDefined(templateUrl = next.templateUrl)) {
if (angular.isFunction(templateUrl)) {
templateUrl = templateUrl(next.params);
}
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
if (angular.isDefined(templateUrl)) {
next.loadedTemplateUrl = templateUrl;
template = $http.get(templateUrl, {cache: $templateCache}).
then(function(response) { return response.data; });
}
}
if (angular.isDefined(template)) {
locals['$template'] = template;
}
return $q.all(locals);
}
}).
// after route change
then(function(locals) {
if (next == $route.current) {
if (next) {
next.locals = locals;
angular.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;
angular.forEach(routes, function(route, path) {
if (!match && (params = switchRouteMatcher($location.path(), route))) {
match = inherit(route, {
params: angular.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 = [];
angular.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#methods_search `search()`} and {@link ng.$location#methods_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);
ngRouteModule.directive('ngView', ngViewFillContentFactory);
/**
* @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
* @priority 400
* @param {string=} onload Expression to evaluate whenever the view updates.
*
* @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll
* $anchorScroll} to scroll the viewport after the view is updated.
*
* - If the attribute is not set, disable scrolling.
* - If the attribute is set without value, enable scrolling.
* - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated
* as an expression yields a truthy value.
* @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="view-animate-container">
<div ng-view class="view-animate"></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">
.view-animate-container {
position:relative;
height:100px!important;
position:relative;
background:white;
border:1px solid black;
height:40px;
overflow:hidden;
}
.view-animate {
padding:10px;
}
.view-animate.ng-enter, .view-animate.ng-leave {
-webkit-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;
}
.view-animate.ng-enter {
left:100%;
}
.view-animate.ng-enter.ng-enter-active {
left:0;
}
.view-animate.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="protractorTest.js">
it('should load and compile correct template', function() {
element(by.linkText('Moby: Ch1')).click();
var content = element(by.css('.doc-example-live [ng-view]')).getText();
expect(content).toMatch(/controller\: ChapterCntl/);
expect(content).toMatch(/Book Id\: Moby/);
expect(content).toMatch(/Chapter Id\: 1/);
element(by.partialLinkText('Scarlet')).click();
content = element(by.css('.doc-example-live [ng-view]')).getText();
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', '$animate'];
function ngViewFactory( $route, $anchorScroll, $animate) {
return {
restrict: 'ECA',
terminal: true,
priority: 400,
transclude: 'element',
link: function(scope, $element, attr, ctrl, $transclude) {
var currentScope,
currentElement,
autoScrollExp = attr.autoscroll,
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 (angular.isDefined(template)) {
var newScope = scope.$new();
var current = $route.current;
// Note: This will also link all children of ng-view that were contained in the original
// html. If that content contains controllers, ... they could pollute/change the scope.
// However, using ng-view on an element with additional content does not make sense...
// Note: We can't remove them in the cloneAttchFn of $transclude as that
// function is called before linking the content, which would apply child
// directives to non existing elements.
var clone = $transclude(newScope, function(clone) {
$animate.enter(clone, null, currentElement || $element, function onNgViewEnter () {
if (angular.isDefined(autoScrollExp)
&& (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll();
}
});
cleanupLastView();
});
currentElement = clone;
currentScope = current.scope = newScope;
currentScope.$emit('$viewContentLoaded');
currentScope.$eval(onloadExp);
} else {
cleanupLastView();
}
}
}
};
}
// This directive is called during the $transclude call of the first `ngView` directive.
// It will replace and compile the content of the element with the loaded template.
// We need this directive so that the element content is already filled when
// the link function of another directive on the same element as ngView
// is called.
ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route'];
function ngViewFillContentFactory($compile, $controller, $route) {
return {
restrict: 'ECA',
priority: -400,
link: function(scope, $element) {
var current = $route.current,
locals = current.locals;
$element.html(locals.$template);
var link = $compile($element.contents());
if (current.controller) {
locals.$scope = scope;
var controller = $controller(current.controller, locals);
if (current.controllerAs) {
scope[current.controllerAs] = controller;
}
$element.data('$ngControllerController', controller);
$element.children().data('$ngControllerController', controller);
}
link(scope);
}
};
}
})(window, window.angular);

View file

@ -1,15 +1,26 @@
/** /**
* @license AngularJS v1.0.7 * @license AngularJS v1.2.13
* (c) 2010-2012 Google, Inc. http://angularjs.org * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT * License: MIT
*/ */
(function(window, angular, undefined) { (function(window, angular, undefined) {'use strict';
'use strict';
var $sanitizeMinErr = angular.$$minErr('$sanitize');
/** /**
* @ngdoc overview * @ngdoc overview
* @name ngSanitize * @name ngSanitize
* @description * @description
*
* # ngSanitize
*
* The `ngSanitize` module provides functionality to sanitize HTML.
*
* {@installModule sanitize}
*
* <div doc-module-components="ngSanitize"></div>
*
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
*/ */
/* /*
@ -40,6 +51,8 @@
* it into the returned string, however, since our parser is more strict than a typical browser * it into the returned string, however, since our parser is more strict than a typical browser
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
* browser, won't make it through the sanitizer. * browser, won't make it through the sanitizer.
* The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
* `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
* *
* @param {string} html Html input. * @param {string} html Html input.
* @returns {string} Sanitized html. * @returns {string} Sanitized html.
@ -48,89 +61,114 @@
<doc:example module="ngSanitize"> <doc:example module="ngSanitize">
<doc:source> <doc:source>
<script> <script>
function Ctrl($scope) { function Ctrl($scope, $sce) {
$scope.snippet = $scope.snippet =
'<p style="color:blue">an html\n' + '<p style="color:blue">an html\n' +
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
'snippet</p>'; 'snippet</p>';
$scope.deliberatelyTrustDangerousSnippet = function() {
return $sce.trustAsHtml($scope.snippet);
};
} }
</script> </script>
<div ng-controller="Ctrl"> <div ng-controller="Ctrl">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table> <table>
<tr> <tr>
<td>Filter</td> <td>Directive</td>
<td>How</td>
<td>Source</td> <td>Source</td>
<td>Rendered</td> <td>Rendered</td>
</tr> </tr>
<tr id="html-filter"> <tr id="bind-html-with-sanitize">
<td>html filter</td> <td>ng-bind-html</td>
<td> <td>Automatically uses $sanitize</td>
<pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre> <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
</td> <td><div ng-bind-html="snippet"></div></td>
<td>
<div ng-bind-html="snippet"></div>
</td>
</tr> </tr>
<tr id="escaped-html"> <tr id="bind-html-with-trust">
<td>no filter</td> <td>ng-bind-html</td>
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
<td>
<pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
&lt;/div&gt;</pre>
</td>
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
</tr>
<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><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind="snippet"></div></td> <td><div ng-bind="snippet"></div></td>
</tr> </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> </table>
</div> </div>
</doc:source> </doc:source>
<doc:scenario> <doc:protractor>
it('should sanitize the html snippet ', function() { it('should sanitize the html snippet by default', function() {
expect(using('#html-filter').element('div').html()). expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
}); });
it('should escape snippet without any filter', function() { it('should inline raw snippet if bound to a trusted value', function() {
expect(using('#escaped-html').element('div').html()). expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
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" + toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>"); "snippet</p>");
}); });
it('should update', function() { it('should escape snippet without any filter', function() {
input('snippet').enter('new <b>text</b>'); expect(element(by.css('#bind-default div')).getInnerHtml()).
expect(using('#html-filter').binding('snippet')).toBe('new <b>text</b>'); toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
expect(using('#escaped-html').element('div').html()).toBe("new &lt;b&gt;text&lt;/b&gt;"); "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
expect(using('#html-unsafe-filter').binding("snippet")).toBe('new <b>text</b>'); "snippet&lt;/p&gt;");
}); });
</doc:scenario>
it('should update', function() {
element(by.model('snippet')).clear();
element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
toBe('new <b>text</b>');
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
'new <b onclick="alert(1)">text</b>');
expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
"new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
});
</doc:protractor>
</doc:example> </doc:example>
*/ */
var $sanitize = function(html) { function $SanitizeProvider() {
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
return function(html) {
var buf = []; var buf = [];
htmlParser(html, htmlSanitizeWriter(buf)); htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
return !/^unsafe/.test($$sanitizeUri(uri, isImage));
}));
return buf.join(''); return buf.join('');
}; };
}];
}
function sanitizeText(chars) {
var buf = [];
var writer = htmlSanitizeWriter(buf, angular.noop);
writer.chars(chars);
return buf.join('');
}
// Regular Expressions for parsing tags and attributes // Regular Expressions for parsing tags and attributes
var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, var START_TAG_REGEXP =
/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
BEGIN_TAG_REGEXP = /^</, BEGIN_TAG_REGEXP = /^</,
BEGING_END_TAGE_REGEXP = /^<\s*\//, BEGING_END_TAGE_REGEXP = /^<\s*\//,
COMMENT_REGEXP = /<!--(.*?)-->/g, COMMENT_REGEXP = /<!--(.*?)-->/g,
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/, // Match everything outside of normal chars and " (quote character)
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
// Good source of info about elements and attributes // Good source of info about elements and attributes
@ -145,23 +183,29 @@ var voidElements = makeMap("area,br,col,hr,img,wbr");
// http://dev.w3.org/html5/spec/Overview.html#optional-tags // http://dev.w3.org/html5/spec/Overview.html#optional-tags
var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
optionalEndTagInlineElements = makeMap("rp,rt"), optionalEndTagInlineElements = makeMap("rp,rt"),
optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); optionalEndTagElements = angular.extend({},
optionalEndTagInlineElements,
optionalEndTagBlockElements);
// Safe Block Elements - HTML5 // Safe Block Elements - HTML5
var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
"blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
"header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
// Inline Elements - HTML5 // Inline Elements - HTML5
var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
"big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
"span,strike,strong,sub,sup,time,tt,u,var")); "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
// Special Elements (can contain anything) // Special Elements (can contain anything)
var specialElements = makeMap("script,style"); var specialElements = makeMap("script,style");
var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); var validElements = angular.extend({},
voidElements,
blockElements,
inlineElements,
optionalEndTagElements);
//Attributes that have href and hence need to be sanitized //Attributes that have href and hence need to be sanitized
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
@ -169,7 +213,7 @@ var validAttrs = angular.extend({}, uriAttrs, makeMap(
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
'scope,scrolling,shape,span,start,summary,target,title,type,'+ 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
'valign,value,vspace,width')); 'valign,value,vspace,width'));
function makeMap(str) { function makeMap(str) {
@ -203,14 +247,22 @@ function htmlParser( html, handler ) {
// Comment // Comment
if ( html.indexOf("<!--") === 0 ) { if ( html.indexOf("<!--") === 0 ) {
index = html.indexOf("-->"); // comments containing -- are not allowed unless they terminate the comment
index = html.indexOf("--", 4);
if ( index >= 0 ) { if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
if (handler.comment) handler.comment( html.substring( 4, index ) ); if (handler.comment) handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 ); html = html.substring( index + 3 );
chars = false; chars = false;
} }
// DOCTYPE
} else if ( DOCTYPE_REGEXP.test(html) ) {
match = html.match( DOCTYPE_REGEXP );
if ( match ) {
html = html.replace( match[0] , '');
chars = false;
}
// end tag // end tag
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) { } else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
match = html.match( END_TAG_REGEXP ); match = html.match( END_TAG_REGEXP );
@ -242,10 +294,9 @@ function htmlParser( html, handler ) {
} }
} else { } else {
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
text = text. function(all, text){
replace(COMMENT_REGEXP, "$1"). text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
replace(CDATA_REGEXP, "$1");
if (handler.chars) handler.chars( decodeEntities(text) ); if (handler.chars) handler.chars( decodeEntities(text) );
@ -256,7 +307,8 @@ function htmlParser( html, handler ) {
} }
if ( html == last ) { 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; last = html;
} }
@ -283,10 +335,11 @@ function htmlParser( html, handler ) {
var attrs = {}; 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 var value = doubleQuotedValue
|| singleQoutedValue || singleQuotedValue
|| unqoutedValue || unquotedValue
|| ''; || '';
attrs[name] = decodeEntities(value); attrs[name] = decodeEntities(value);
@ -314,15 +367,32 @@ function htmlParser( html, handler ) {
} }
} }
var hiddenPre=document.createElement("pre");
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
/** /**
* decodes all entities into regular string * decodes all entities into regular string
* @param value * @param value
* @returns {string} A string with decoded entities. * @returns {string} A string with decoded entities.
*/ */
var hiddenPre=document.createElement("pre");
function decodeEntities(value) { function decodeEntities(value) {
hiddenPre.innerHTML=value.replace(/</g,"&lt;"); if (!value) { return ''; }
return hiddenPre.innerText || hiddenPre.textContent || '';
// Note: IE8 does not preserve spaces at the start/end of innerHTML
// so we must capture them and reattach them afterward
var parts = spaceRe.exec(value);
var spaceBefore = parts[1];
var spaceAfter = parts[3];
var content = parts[2];
if (content) {
hiddenPre.innerHTML=content.replace(/</g,"&lt;");
// innerText depends on styling as it doesn't display hidden elements.
// Therefore, it's better to use textContent not to cause unnecessary
// reflows. However, IE<9 don't support textContent so the innerText
// fallback is necessary.
content = 'textContent' in hiddenPre ?
hiddenPre.textContent : hiddenPre.innerText;
}
return spaceBefore + content + spaceAfter;
} }
/** /**
@ -352,7 +422,7 @@ function encodeEntities(value) {
* comment: function(text) {} * comment: function(text) {}
* } * }
*/ */
function htmlSanitizeWriter(buf){ function htmlSanitizeWriter(buf, uriValidator){
var ignore = false; var ignore = false;
var out = angular.bind(buf, buf.push); var out = angular.bind(buf, buf.push);
return { return {
@ -361,12 +431,14 @@ function htmlSanitizeWriter(buf){
if (!ignore && specialElements[tag]) { if (!ignore && specialElements[tag]) {
ignore = tag; ignore = tag;
} }
if (!ignore && validElements[tag] == true) { if (!ignore && validElements[tag] === true) {
out('<'); out('<');
out(tag); out(tag);
angular.forEach(attrs, function(value, key){ angular.forEach(attrs, function(value, key){
var lkey=angular.lowercase(key); var lkey=angular.lowercase(key);
if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
if (validAttrs[lkey] === true &&
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
out(' '); out(' ');
out(key); out(key);
out('="'); out('="');
@ -379,7 +451,7 @@ function htmlSanitizeWriter(buf){
}, },
end: function(tag){ end: function(tag){
tag = angular.lowercase(tag); tag = angular.lowercase(tag);
if (!ignore && validElements[tag] == true) { if (!ignore && validElements[tag] === true) {
out('</'); out('</');
out(tag); out(tag);
out('>'); out('>');
@ -398,30 +470,9 @@ function htmlSanitizeWriter(buf){
// define ngSanitize module and register $sanitize service // define ngSanitize module and register $sanitize service
angular.module('ngSanitize', []).value('$sanitize', $sanitize); angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
/** /* global sanitizeText: false */
* @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 * @ngdoc filter
@ -432,7 +483,10 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
* plain email address links. * plain email address links.
* *
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
*
* @param {string} text Input text. * @param {string} text Input text.
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
* @returns {string} Html-linkified text. * @returns {string} Html-linkified text.
* *
* @usage * @usage
@ -449,6 +503,7 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san
'mailto:us@somewhere.org,\n'+ 'mailto:us@somewhere.org,\n'+
'another@somewhere.org,\n'+ 'another@somewhere.org,\n'+
'and one more: ftp://127.0.0.1/.'; 'and one more: ftp://127.0.0.1/.';
$scope.snippetWithTarget = 'http://angularjs.org/';
} }
</script> </script>
<div ng-controller="Ctrl"> <div ng-controller="Ctrl">
@ -468,6 +523,15 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san
<div ng-bind-html="snippet | linky"></div> <div ng-bind-html="snippet | linky"></div>
</td> </td>
</tr> </tr>
<tr id="linky-target">
<td>linky target</td>
<td>
<pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
</td>
<td>
<div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
</td>
</tr>
<tr id="escaped-html"> <tr id="escaped-html">
<td>no filter</td> <td>no filter</td>
<td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td> <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
@ -475,45 +539,50 @@ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($san
</tr> </tr>
</table> </table>
</doc:source> </doc:source>
<doc:scenario> <doc:protractor>
it('should linkify the snippet with urls', function() { it('should linkify the snippet with urls', function() {
expect(using('#linky-filter').binding('snippet | linky')). expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
toBe('Pretty text with some links:&#10;' + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
'<a href="http://angularjs.org/">http://angularjs.org/</a>,&#10;' + 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
'<a href="mailto:us@somewhere.org">us@somewhere.org</a>,&#10;' + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
'<a href="mailto:another@somewhere.org">another@somewhere.org</a>,&#10;' +
'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
}); });
it ('should not linkify snippet without the linky filter', function() { it('should not linkify snippet without the linky filter', function() {
expect(using('#escaped-html').binding('snippet')). expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
toBe("Pretty text with some links:\n" + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
"http://angularjs.org/,\n" + 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
"mailto:us@somewhere.org,\n" + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
"another@somewhere.org,\n" +
"and one more: ftp://127.0.0.1/.");
}); });
it('should update', function() { it('should update', function() {
input('snippet').enter('new http://link.'); element(by.model('snippet')).clear();
expect(using('#linky-filter').binding('snippet | linky')). element(by.model('snippet')).sendKeys('new http://link.');
toBe('new <a href="http://link">http://link</a>.'); expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); toBe('new http://link.');
expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
.toBe('new http://link.');
}); });
</doc:scenario>
it('should work with the target property', function() {
expect(element(by.id('linky-target')).
element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
toBe('http://angularjs.org/');
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
});
</doc:protractor>
</doc:example> </doc:example>
*/ */
angular.module('ngSanitize').filter('linky', function() { angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,
MAILTO_REGEXP = /^mailto:/; MAILTO_REGEXP = /^mailto:/;
return function(text) { return function(text, target) {
if (!text) return text; if (!text) return text;
var match; var match;
var raw = text; var raw = text;
var html = []; var html = [];
// TODO(vojta): use $sanitize instead
var writer = htmlSanitizeWriter(html);
var url; var url;
var i; var i;
while ((match = raw.match(LINKY_URL_REGEXP))) { while ((match = raw.match(LINKY_URL_REGEXP))) {
@ -522,16 +591,35 @@ angular.module('ngSanitize').filter('linky', function() {
// if we did not match ftp/http/mailto then assume mailto // if we did not match ftp/http/mailto then assume mailto
if (match[2] == match[3]) url = 'mailto:' + url; if (match[2] == match[3]) url = 'mailto:' + url;
i = match.index; i = match.index;
writer.chars(raw.substr(0, i)); addText(raw.substr(0, i));
writer.start('a', {href:url}); addLink(url, match[0].replace(MAILTO_REGEXP, ''));
writer.chars(match[0].replace(MAILTO_REGEXP, ''));
writer.end('a');
raw = raw.substring(i + match[0].length); raw = raw.substring(i + match[0].length);
} }
writer.chars(raw); addText(raw);
return html.join(''); return $sanitize(html.join(''));
function addText(text) {
if (!text) {
return;
}
html.push(sanitizeText(text));
}
function addLink(url, text) {
html.push('<a ');
if (angular.isDefined(target)) {
html.push('target="');
html.push(target);
html.push('" ');
}
html.push('href="');
html.push(url);
html.push('">');
addText(text);
html.push('</a>');
}
}; };
}); }]);
})(window, window.angular); })(window, window.angular);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -12,16 +12,17 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
} }
return { return {
require: 'ngModel', require: 'ngModel',
priority: 1,
compile: function (tElm, tAttrs) { compile: function (tElm, tAttrs) {
var watch, var watch,
repeatOption, repeatOption,
repeatAttr, repeatAttr,
isSelect = tElm.is('select'), isSelect = tElm.is('select'),
isMultiple = (tAttrs.multiple !== undefined); isMultiple = angular.isDefined(tAttrs.multiple);
// Enable watching of the options dataset if in use // Enable watching of the options dataset if in use
if (tElm.is('select')) { if (tElm.is('select')) {
repeatOption = tElm.find('option[ng-repeat], option[data-ng-repeat]'); repeatOption = tElm.find( 'optgroup[ng-repeat], optgroup[data-ng-repeat], option[ng-repeat], option[data-ng-repeat]');
if (repeatOption.length) { if (repeatOption.length) {
repeatAttr = repeatOption.attr('ng-repeat') || repeatOption.attr('data-ng-repeat'); repeatAttr = repeatOption.attr('ng-repeat') || repeatOption.attr('data-ng-repeat');
@ -39,21 +40,21 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
var convertToAngularModel = function(select2_data) { var convertToAngularModel = function(select2_data) {
var model; var model;
if (opts.simple_tags) { if (opts.simple_tags) {
model = [] model = [];
angular.forEach(select2_data, function(value, index) { angular.forEach(select2_data, function(value, index) {
model.push(value.id) model.push(value.id);
}) });
} else { } else {
model = select2_data model = select2_data;
}
return model
} }
return model;
};
/* /*
Convert from Angular view-model to Select2 view-model. Convert from Angular view-model to Select2 view-model.
*/ */
var convertToSelect2Model = function(angular_data) { var convertToSelect2Model = function(angular_data) {
var model = [] var model = [];
if (!angular_data) { if (!angular_data) {
return model; return model;
} }
@ -64,12 +65,12 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
angular_data, angular_data,
function(value, index) { function(value, index) {
model.push({'id': value, 'text': value}); model.push({'id': value, 'text': value});
}) });
} else { } else {
model = angular_data; model = angular_data;
} }
return model return model;
} };
if (isSelect) { if (isSelect) {
// Use <select multiple> instead // Use <select multiple> instead
@ -83,20 +84,24 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
// Watch the model for programmatic changes // Watch the model for programmatic changes
scope.$watch(tAttrs.ngModel, function(current, old) { scope.$watch(tAttrs.ngModel, function(current, old) {
if (!current) { if (!current) {
return return;
} }
if (current == old) { if (current === old) {
return return;
} }
controller.$render() controller.$render();
}, true) }, true);
controller.$render = function () { controller.$render = function () {
if (isSelect) { if (isSelect) {
elm.select2('val', controller.$viewValue); elm.select2('val', controller.$viewValue);
} else { } else {
if (opts.multiple) { if (opts.multiple) {
var viewValue = controller.$viewValue;
if (angular.isString(viewValue)) {
viewValue = viewValue.split(',');
}
elm.select2( elm.select2(
'data', convertToSelect2Model(controller.$viewValue)); 'data', convertToSelect2Model(viewValue));
} else { } else {
if (angular.isObject(controller.$viewValue)) { if (angular.isObject(controller.$viewValue)) {
elm.select2('data', controller.$viewValue); elm.select2('data', controller.$viewValue);
@ -112,19 +117,24 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
// Watch the options dataset for changes // Watch the options dataset for changes
if (watch) { if (watch) {
scope.$watch(watch, function (newVal, oldVal, scope) { scope.$watch(watch, function (newVal, oldVal, scope) {
if (!newVal) return; if (angular.equals(newVal, oldVal)) {
return;
}
// Delayed so that the options have time to be rendered // Delayed so that the options have time to be rendered
$timeout(function () { $timeout(function () {
elm.select2('val', controller.$viewValue); elm.select2('val', controller.$viewValue);
// Refresh angular to remove the superfluous option // Refresh angular to remove the superfluous option
elm.trigger('change'); elm.trigger('change');
if(newVal && !oldVal && controller.$setPristine) {
controller.$setPristine(true);
}
}); });
}); });
} }
// Update valid and dirty statuses // Update valid and dirty statuses
controller.$parsers.push(function (value) { controller.$parsers.push(function (value) {
var div = elm.prev() var div = elm.prev();
div div
.toggleClass('ng-invalid', !controller.$valid) .toggleClass('ng-invalid', !controller.$valid)
.toggleClass('ng-valid', controller.$valid) .toggleClass('ng-valid', controller.$valid)
@ -137,8 +147,12 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
if (!isSelect) { if (!isSelect) {
// Set the view and model value and update the angular template manually for the ajax/multiple select2. // Set the view and model value and update the angular template manually for the ajax/multiple select2.
elm.bind("change", function () { elm.bind("change", function (e) {
if (scope.$$phase) return; e.stopImmediatePropagation();
if (scope.$$phase || scope.$root.$$phase) {
return;
}
scope.$apply(function () { scope.$apply(function () {
controller.$setViewValue( controller.$setViewValue(
convertToAngularModel(elm.select2('data'))); convertToAngularModel(elm.select2('data')));
@ -149,8 +163,13 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
var initSelection = opts.initSelection; var initSelection = opts.initSelection;
opts.initSelection = function (element, callback) { opts.initSelection = function (element, callback) {
initSelection(element, function (value) { initSelection(element, function (value) {
var isPristine = controller.$pristine;
controller.$setViewValue(convertToAngularModel(value)); controller.$setViewValue(convertToAngularModel(value));
callback(value); callback(value);
if (isPristine) {
controller.$setPristine();
}
elm.prev().toggleClass('ng-pristine', controller.$pristine);
}); });
}; };
} }
@ -171,6 +190,7 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
if (attrs.ngMultiple) { if (attrs.ngMultiple) {
scope.$watch(attrs.ngMultiple, function(newVal) { scope.$watch(attrs.ngMultiple, function(newVal) {
attrs.$set('multiple', !!newVal);
elm.select2(opts); elm.select2(opts);
}); });
} }
@ -180,14 +200,21 @@ angular.module('ui.select2', []).value('uiSelect2Config', {}).directive('uiSelec
elm.select2(opts); elm.select2(opts);
// Set initial value - I'm not sure about this but it seems to need to be there // Set initial value - I'm not sure about this but it seems to need to be there
elm.val(controller.$viewValue); elm.select2('data', controller.$modelValue);
// important! // important!
controller.$render(); controller.$render();
// Not sure if I should just check for !isSelect OR if I should check for 'tags' key // Not sure if I should just check for !isSelect OR if I should check for 'tags' key
if (!opts.initSelection && !isSelect) if (!opts.initSelection && !isSelect) {
var isPristine = controller.$pristine;
controller.$setViewValue( controller.$setViewValue(
convertToAngularModel(elm.select2('data'))); convertToAngularModel(elm.select2('data'))
);
if (isPristine) {
controller.$setPristine();
}
elm.prev().toggleClass('ng-pristine', controller.$pristine);
}
}); });
}; };
} }

View file

@ -72,23 +72,27 @@
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="loginTheme">Login Theme</label> <label class="col-sm-2 control-label" for="loginTheme">Login Theme</label>
<div class="col-sm-4"> <div class="col-sm-4">
<select kc-select id="loginTheme" <div class="select-kc">
data-kc-placeholder="Select one..." <select id="loginTheme"
data-kc-model="realm.loginTheme" ng-model="realm.loginTheme"
data-kc-options="serverInfo.themes.login"> ng-options="o as o for o in serverInfo.themes.login">
<option value="" disabled selected>Select one...</option>
</select> </select>
</div> </div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="accountTheme">Account Theme</label> <label class="col-sm-2 control-label" for="accountTheme">Account Theme</label>
<div class="col-sm-4"> <div class="col-sm-4">
<select kc-select id="accountTheme" <div class="select-kc">
data-kc-placeholder="Select one..." <select id="accountTheme"
data-kc-model="realm.accountTheme" ng-model="realm.accountTheme"
data-kc-options="serverInfo.themes.account"> ng-options="o as o for o in serverInfo.themes.account">
<option value="" disabled selected>Select one...</option>
</select> </select>
</div> </div>
</div> </div>
</div>
</fieldset> </fieldset>
<div class="pull-right form-actions" data-ng-show="createRealm && access.manageRealm"> <div class="pull-right form-actions" data-ng-show="createRealm && access.manageRealm">

View file

@ -0,0 +1,12 @@
<span>
<div class='onoffswitch' tabindex='0'>
<input type='checkbox' ng-model='ngModel' class='onoffswitch-checkbox' name='{{name}}' id='{{id}}'>
<label for='{{id}}' class='onoffswitch-label'>
<span class='onoffswitch-inner'>
<span class='onoffswitch-active'>{{kcOnText}}</span>
<span class='onoffswitch-inactive'>{{kcOffText}}</span>
</span>
<span class='onoffswitch-switch'></span>
</label>
</div>
</span>