├── Plugin.php ├── README.md ├── assets ├── js │ ├── angular-bridge.js │ └── angular-retired-code.js └── vendor │ └── angular │ ├── angular-animate.js │ ├── angular-route.js │ ├── angular-sanitize.js │ └── angular.js ├── classes ├── PageScript.php └── ScopeBag.php ├── components ├── Layout.php └── layout │ └── default.htm └── updates └── version.yaml /Plugin.php: -------------------------------------------------------------------------------- 1 | 'Angular', 24 | 'description' => 'Tools for working with AngularJS', 25 | 'author' => 'Responsiv Internet', 26 | 'icon' => 'icon-leaf' 27 | ]; 28 | } 29 | 30 | public function boot() 31 | { 32 | // sleep(1); 33 | Event::listen('cms.page.beforeDisplay', function($controller, $url, $page) { 34 | if ($params = post('X_OCTOBER_NG_PARAMS')) { 35 | $controller->getRouter()->setParameters($params); 36 | } 37 | }); 38 | 39 | Event::listen('cms.page.display', function($controller, $url, $page) { 40 | if ( 41 | array_key_exists('ng-page', Input::all()) && 42 | $controller->getAjaxHandler() === null 43 | ) { 44 | if ($content = $controller->renderPage()) { 45 | return $content; 46 | } 47 | 48 | // If we don't return something, this will cause an infinite loop 49 | return ''; 50 | } 51 | }); 52 | 53 | Event::listen('cms.page.init', function($controller, $page) { 54 | if ($partial = post('ng-partial')) { 55 | return $controller->renderPartial($partial); 56 | } 57 | }); 58 | 59 | Event::listen('backend.form.extendFields', function($widget) { 60 | if (!$widget->getController() instanceof \Cms\Controllers\Index) return; 61 | if (!$widget->model instanceof \Cms\Classes\Page) return; 62 | 63 | PageScript::fromTemplate($widget->model)->populate(); 64 | 65 | $widget->addFields([ 66 | 'script' => [ 67 | 'tab' => 'Script', 68 | 'stretch' => 'true', 69 | 'type' => 'codeeditor', 70 | 'language' => 'javascript', 71 | ] 72 | ], 'secondary'); 73 | }); 74 | 75 | Event::listen('cms.template.save', function($controller, $template, $type){ 76 | if ($type != 'page') return; 77 | 78 | $script = PageScript::fromTemplate($template); 79 | $script->save(post('script')); 80 | }); 81 | } 82 | 83 | public function registerComponents() 84 | { 85 | return [ 86 | '\Responsiv\Angular\Components\Layout' => 'appLayout', 87 | ]; 88 | } 89 | 90 | } 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Plugin 2 | 3 | Tools useful for creating websties with AngularJS and OctoberCMS. 4 | 5 | ## High level design consideration 6 | 7 | Angular is an excellent front-end framework for building Single Page Applications (SPAs) and is also fantastic as a templating engine that binds HTML and JavaScript together. With this plugin you can do the following: 8 | 9 | - Create a complete Single Page Application (SPA). 10 | - Create AJAX-enabled sections of the website that behave like SPA (AJAX sections). 11 | - Leverage AngularJS for your front-end templating. 12 | 13 | When using this plugin in October, the page layout acts as the entry page. This allows a website to act as **multiple SPAs** and also as a traditional website. A complete SPA website should use just one layout, whereas a hybrid website could use a layout for each AJAX section along with layouts for the traditional pages. See the Layout component definition below for a technical overview of this approach. 14 | 15 | ## Templating 16 | 17 | It is important to note that Angular's templating engine conflicts with October's Twig engine, although the syntax is pleasingly similar. To work around this use `{% verbatim %}` and `{% endverbatim %}` to wrap your Angular code. 18 | 19 |

Hello {{ name }}, this is parsed by Twig

20 | 21 | {% verbatim %} 22 |

Hello {{ name }}, this is parsed by Angular

23 | {% endverbatim %} 24 | 25 | ## The $request service|provider 26 | 27 | By injecting the `$request` dependancy in to your Angular controller, you can access to October's AJAX framework. 28 | 29 | function ($scope, $request) { 30 | $scope.customers = $request('onGetCustomers') 31 | } 32 | 33 | In almost all cases the AJAX function should return a JSON object or array for use in JavaScript. 34 | 35 | function onGetCustomers() 36 | { 37 | return Customers::all()->toJson(); 38 | } 39 | 40 | ## Layout component 41 | 42 | The Layout component should be attached to a layout, it is not recommended to attach it to pages. This allocates the layout as having the intended use of being a single-page-application (SPA). The Angular route table will be comprised of all pages that belong to the layout. 43 | 44 | It should define a master page that will act as the primary HTML template and should contain will have the **ng-view** declaration inside. The master page should use a base url, all other pages that use the layout must be prefixed with this url. So if the master page URL is `/submit` then all other pages must have a url that starts with `/submit`, for example, `/submit/step1`. 45 | 46 | When the component is rendered, it will define a JavaScript object using the same name of the component alias, this defaults to `appLayout`. It will also generate a route table for all the pages that are assigned to the layout. 47 | 48 | #### Example structure 49 | 50 | Here is an example file structure: 51 | 52 | layouts/ 53 | submit.htm <== description: Submission process 54 | 55 | pages/ 56 | start.htm <== url: /submit, layout: submit 57 | step1.htm <== url: /submit/step1, layout: submit 58 | step2.htm <== url: /submit/step2, layout: submit 59 | finish.htm <== url: /submit/finish, layout: submit 60 | 61 | Here is an example of the layouts/submit.htm contents: 62 | 63 | description: Submission process 64 | 65 | [appLayout] 66 | masterPage = "submit" 67 | == 68 | 69 | 70 | [...] 71 | 72 | 73 | 74 | 75 | {% component 'appLayout' %} 76 | 77 | 78 | 79 | In this example the Layout component is attached to the **layouts/submit.htm** layout with the master page being set to **pages/start.htm**. The component will render a route table similar to this: 80 | 81 | var appLayout = angular.module('appLayout', ['ngRoute']) 82 | 83 | appLayout.config(['$routeProvider', function($routeProvider) { 84 | $routeProvider.when('/submit', { templateUrl: 'submit', controller: '...' }); 85 | $routeProvider.when('/submit/step1', { templateUrl: 'submit/step1', controller: '...' }); 86 | $routeProvider.when('/submit/step2', { templateUrl: 'submit/step2', controller: '...' }); 87 | $routeProvider.when('/submit/finish', { templateUrl: 'submit/finish', controller: '...' }); 88 | $routeProvider.otherwise({ redirectTo: '/submit' }); 89 | }) 90 | 91 | > **Note:** When angular accesses the **templateUrl**, October will automatically provide the Page content without the Layout. 92 | 93 | ## Page controllers 94 | 95 | Each page is extended to include a **Script** tab inside the CMS that is defined as an anonymous function. 96 | 97 | function ($scope) { 98 | // 99 | // Page logic 100 | // 101 | } 102 | 103 | This translates to 104 | 105 | october.controllers['page/filename'] = function($scope) { ... } 106 | 107 | -------------------------------------------------------------------------------- /assets/js/angular-bridge.js: -------------------------------------------------------------------------------- 1 | /* ======================================================================== 2 | * October CMS: AngularJS bridge script 3 | * http://octobercms.com 4 | * ======================================================================== 5 | * Copyright 2014 Alexey Bobkov, Samuel Georges 6 | * 7 | * ======================================================================== */ 8 | 9 | if (typeof angular == 'undefined') 10 | throw new Error('The AngularJS library is not loaded. The October-Angular bridge cannot be initialized.'); 11 | 12 | +function (angular, window) { "use strict"; 13 | 14 | var OctoberAngular = function() { 15 | 16 | var o = { 17 | 18 | app: null, 19 | 20 | controllers: {}, 21 | 22 | assets: {}, 23 | 24 | useStripeLoadIndicator: true, 25 | 26 | bootApp: function (app) { 27 | o.app = app 28 | 29 | app.run(['$rootScope','$location', '$routeParams', function($rootScope, $location, $routeParams) { 30 | 31 | $rootScope.$on('$routeChangeSuccess', function(e, current, pre) { 32 | console.log('Current route name: ' + $location.path()); 33 | }) 34 | 35 | /* 36 | * Loading indicator 37 | */ 38 | $rootScope.$on('$routeChangeSuccess', function(e, current, pre) { 39 | if (o.useStripeLoadIndicator && jQuery.oc.stripeLoadIndicator !== undefined) 40 | jQuery.oc.stripeLoadIndicator.hide() 41 | }) 42 | $rootScope.$on('$routeChangeStart', function(e, current, pre) { 43 | if (o.useStripeLoadIndicator && jQuery.oc.stripeLoadIndicator !== undefined) 44 | jQuery.oc.stripeLoadIndicator.show() 45 | }) 46 | 47 | }]) 48 | }, 49 | 50 | // 51 | // This function will load the assets for a page, including the controller 52 | // script and execute onComplete function once everything is good to go. 53 | // 54 | loadPage: function (baseFilename, url, params, onComplete){ 55 | var options = {} 56 | options.url = url 57 | options.data = { 58 | X_OCTOBER_NG_PARAMS: params 59 | } 60 | options.success = function(data) { 61 | this.success(data).done(function(){ 62 | if (!o.controllers[baseFilename]) 63 | o.controllers[baseFilename] = function(){} 64 | 65 | o.app.register.controller(baseFilename, o.controllers[baseFilename]) 66 | onComplete(data) 67 | }) 68 | } 69 | 70 | $.request('onGetPageDependencies', options) 71 | } 72 | 73 | } 74 | 75 | return o 76 | } 77 | 78 | window.october = new OctoberAngular 79 | 80 | }(angular, window); 81 | 82 | /* 83 | * October-Angular Services 84 | */ 85 | 86 | +function (angular, october) { "use strict"; 87 | 88 | var services = angular.module('ocServices', []) 89 | 90 | /* 91 | * AJAX framework 92 | */ 93 | services.service('$request', ['$rootScope', '$route', function($rootScope, $route){ 94 | return function(handler, option) { 95 | 96 | var requestOptions = { 97 | url: $route.current.loadedTemplateUrl, 98 | data: { 99 | X_OCTOBER_NG_PARAMS: $route.current.params 100 | } 101 | } 102 | 103 | /* 104 | * Short hand call 105 | */ 106 | if (typeof option == 'function') { 107 | return $.request(handler, requestOptions).done(function(data, textStatus, jqXHR){ 108 | var singularData = data.result ? data.result : data 109 | option(singularData, data, textStatus, jqXHR) 110 | $rootScope.$apply() 111 | }) 112 | } 113 | 114 | /* 115 | * Standard call 116 | */ 117 | return $.request(handler, $.extend(true, requestOptions, option)).done(function(){ 118 | // Make lowest priority 119 | setTimeout(function(){ $rootScope.$apply() }, 0) 120 | }) 121 | } 122 | }]) 123 | 124 | /* 125 | * Partial loading 126 | */ 127 | services.directive('ocPartial', [function () { 128 | return { 129 | link: function(scope, element, attr, controllers) { 130 | scope.partialUrl = '?ng-partial='+attr.ocPartial 131 | }, 132 | transclude: true, 133 | template: '
' 134 | } 135 | }]) 136 | 137 | /* 138 | * CMS Router 139 | */ 140 | services.provider('$cmsRouter', function () { 141 | 142 | this.$get = function () { 143 | return this 144 | } 145 | 146 | this.routeConfig = function () { 147 | var baseUrl = '/', 148 | routeMap = [], 149 | 150 | setBaseUrl = function (url) { 151 | baseUrl = url 152 | }, 153 | 154 | getBaseUrl = function () { 155 | return baseUrl 156 | }, 157 | 158 | mapPage = function(pageName, routePattern, viewUrl) { 159 | routeMap.push({ 160 | name: pageName, 161 | pattern: routePattern, 162 | view: viewUrl 163 | }) 164 | }, 165 | 166 | getPageName = function(routePattern) { 167 | return fetchValueWhere('name', 'pattern', routePattern) 168 | }, 169 | 170 | getPageView = function(pageName) { 171 | return fetchValueWhere('view', 'name', pageName) 172 | }, 173 | 174 | getPagePattern = function(pageName) { 175 | return fetchValueWhere('pattern', 'name', pageName) 176 | }, 177 | 178 | fetchValueWhere = function(fetch, key, value) { 179 | var count = routeMap.length 180 | for (var i = 0; i < count; ++i) { 181 | if (routeMap[i][key] == value) 182 | return routeMap[i][fetch] 183 | } 184 | 185 | return null 186 | } 187 | 188 | return { 189 | setBaseUrl: setBaseUrl, 190 | getBaseUrl: getBaseUrl, 191 | mapPage: mapPage, 192 | getPageName: getPageName, 193 | getPageView: getPageView, 194 | getPagePattern: getPagePattern 195 | } 196 | }() 197 | 198 | 199 | this.route = function (routeConfig, helper) { 200 | 201 | var resolve = function (baseName) { 202 | var routeDef = {} 203 | 204 | routeDef.templateUrl = function(params) { return makeTemplateUrl(baseName, params) } 205 | routeDef.controller = baseName 206 | routeDef.resolve = { 207 | load: ['$q', '$rootScope', '$route', function ($q, $rootScope, $route) { 208 | return resolveDependencies($q, $rootScope, $route, baseName) 209 | }] 210 | } 211 | 212 | return routeDef 213 | }, 214 | 215 | resolveDependencies = function ($q, $rootScope, $route, baseName) { 216 | var url = routeConfig.getPageView(baseName), 217 | params = $route.current.params, 218 | defer = $q.defer() 219 | 220 | october.loadPage(baseName, url, params, function(data){ 221 | defer.resolve() 222 | $rootScope.$apply(function($scope){ 223 | $scope = angular.extend($scope, angular.copy(data.scope)) 224 | }) 225 | }) 226 | 227 | return defer.promise; 228 | }, 229 | 230 | makeTemplateUrl = function (baseName, params) { 231 | var url = routeConfig.getPageView(baseName) 232 | return url + '?ng-page' 233 | } 234 | 235 | return { 236 | resolve: resolve 237 | } 238 | }(this.routeConfig) 239 | 240 | }) 241 | 242 | }(angular, window.october); 243 | 244 | 245 | /* 246 | * Asset Manager 247 | * 248 | * Usage: assetManager.load({ css:[], js:[], img:[] }, onLoadedCallback) 249 | */ 250 | 251 | AssetManager = function() { 252 | 253 | var o = { 254 | 255 | load: function(collection, callback) { 256 | var jsList = (collection.js) ? collection.js : [], 257 | cssList = (collection.css) ? collection.css : [], 258 | imgList = (collection.img) ? collection.img : [] 259 | 260 | jsList = $.grep(jsList, function(item){ 261 | return $('head script[src="'+item+'"]').length == 0 262 | }) 263 | 264 | cssList = $.grep(cssList, function(item){ 265 | return $('head link[href="'+item+'"]').length == 0 266 | }) 267 | 268 | var cssCounter = 0, 269 | jsLoaded = false, 270 | imgLoaded = false 271 | 272 | if (jsList.length === 0 && cssList.length === 0 && imgList.length === 0) { 273 | callback && callback() 274 | return 275 | } 276 | 277 | o.loadJavaScript(jsList, function(){ 278 | jsLoaded = true 279 | checkLoaded() 280 | }) 281 | 282 | $.each(cssList, function(index, source){ 283 | o.loadStyleSheet(source, function(){ 284 | cssCounter++ 285 | checkLoaded() 286 | }) 287 | }) 288 | 289 | o.loadImage(imgList, function(){ 290 | imgLoaded = true 291 | checkLoaded() 292 | }) 293 | 294 | function checkLoaded() { 295 | if (!imgLoaded) 296 | return false 297 | 298 | if (!jsLoaded) 299 | return false 300 | 301 | if (cssCounter < cssList.length) 302 | return false 303 | 304 | callback && callback() 305 | } 306 | }, 307 | 308 | /* 309 | * Loads StyleSheet files 310 | */ 311 | loadStyleSheet: function(source, callback) { 312 | var cssElement = document.createElement('link') 313 | 314 | cssElement.setAttribute('rel', 'stylesheet') 315 | cssElement.setAttribute('type', 'text/css') 316 | cssElement.setAttribute('href', source) 317 | cssElement.addEventListener('load', callback, false) 318 | 319 | if (typeof cssElement != 'undefined') { 320 | document.getElementsByTagName('head')[0].appendChild(cssElement) 321 | } 322 | 323 | return cssElement 324 | }, 325 | 326 | /* 327 | * Loads JavaScript files in sequence 328 | */ 329 | loadJavaScript: function(sources, callback) { 330 | if (sources.length <= 0) 331 | return callback() 332 | 333 | var source = sources.shift(), 334 | jsElement = document.createElement('script'); 335 | 336 | jsElement.setAttribute('type', 'text/javascript') 337 | jsElement.setAttribute('src', source) 338 | jsElement.addEventListener('load', function() { 339 | o.loadJavaScript(sources, callback) 340 | }, false) 341 | 342 | if (typeof jsElement != 'undefined') { 343 | document.getElementsByTagName('head')[0].appendChild(jsElement) 344 | } 345 | }, 346 | 347 | /* 348 | * Loads Image files 349 | */ 350 | loadImage: function(sources, callback) { 351 | if (sources.length <= 0) 352 | return callback() 353 | 354 | var loaded = 0 355 | $.each(sources, function(index, source){ 356 | var img = new Image() 357 | img.onload = function() { 358 | if (++loaded == sources.length && callback) 359 | callback() 360 | } 361 | img.src = source 362 | }) 363 | } 364 | 365 | }; 366 | 367 | return o; 368 | }; 369 | 370 | assetManager = new AssetManager(); 371 | -------------------------------------------------------------------------------- /assets/js/angular-retired-code.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This is code that could be useful in future but is not 3 | * included in the name of K.I.S.S. 4 | */ 5 | 6 | /* 7 | * Filters 8 | */ 9 | services.filter('page', function($cmsRouter){ 10 | return function (input, params, routePersistence) { 11 | return $cmsRouter.route.pageUrl(input, params); 12 | } 13 | }) 14 | 15 | /* 16 | * Blocks 17 | */ 18 | services.service('$cmsBlocks', function($rootScope, $compile, $animate){ 19 | 20 | var o = {} 21 | 22 | o.names = [] 23 | o.map = {} 24 | o.puts = {} 25 | 26 | o.put = function (name, element) { 27 | o.puts[name] = element 28 | 29 | if (o.map[name] !== undefined) 30 | o.replaceContent(o.map[name], element, name) 31 | } 32 | 33 | o.placeholder = function (name, element) { 34 | o.names.push(name) 35 | o.map[name] = element 36 | 37 | if (o.puts[name] !== undefined) 38 | o.replaceContent(element, o.puts[name], name) 39 | } 40 | 41 | o.replaceContent = function($element, src, name) { 42 | var clone = $element.clone().html(src.html()).removeClass('ng-leave') 43 | $compile(clone.contents())($rootScope) 44 | 45 | $animate.enter(clone, null, $element); 46 | $animate.leave($element) 47 | o.map[name] = clone 48 | 49 | $rootScope.$emit('$cmsBlocksReplceContent', name) 50 | } 51 | 52 | $rootScope.$on('$routeChangeSuccess', function(){ 53 | o.puts = {} 54 | for (var i = 0; i < o.names.length; ++i) { 55 | var name = o.names[i] 56 | if (o.map[name] !== undefined) { 57 | var $element = o.map[name], 58 | clone = $element.clone().empty().removeClass('ng-leave') 59 | 60 | $element.after(clone) 61 | $animate.leave($element) 62 | o.map[name] = clone 63 | } 64 | } 65 | }) 66 | 67 | return o 68 | }) 69 | 70 | services.directive('ocPut', ['$cmsBlocks', function ($cmsBlocks) { 71 | return { 72 | link: function(scope, element, attr, controllers) { 73 | $cmsBlocks.put(attr.ocPut, element.clone()) 74 | element.remove() 75 | } 76 | } 77 | }]) 78 | 79 | services.directive('ocPlaceholder', ['$cmsBlocks', function ($cmsBlocks) { 80 | return { 81 | link: function(scope, element, attr, controllers) { 82 | $cmsBlocks.placeholder(attr.ocPlaceholder, element) 83 | } 84 | } 85 | }]) 86 | 87 | /* 88 | * CMS Router 89 | */ 90 | services.provider('$cmsRouter', function () { 91 | 92 | 93 | this.helper = function (routeConfig) { 94 | 95 | var normalizeUrl = function(url) { 96 | if (url.substring(0, 1) != '/') 97 | url = '/' + url 98 | 99 | if (url.substring(-1) == '/') 100 | url = url.substring(0, -1) 101 | 102 | if (!url) 103 | url = '/' 104 | 105 | return url 106 | }, 107 | 108 | segmentizeUrl = function(url) { 109 | url = normalizeUrl(url) 110 | var parts = url.split('/'), 111 | result = [] 112 | 113 | angular.forEach(parts, function(segment){ 114 | if (segment) 115 | result.push(segment) 116 | }) 117 | 118 | return result 119 | }, 120 | 121 | getParamName = function(segment) { 122 | var name = segment.substring(1), 123 | optMarkerPos = name.indexOf('?'), 124 | regexMarkerPos = name.indexOf('|') 125 | 126 | if (optMarkerPos != -1 && regexMarkerPos != -1) { 127 | if (optMarkerPos < regexMarkerPos) 128 | return name.substring(0, optMarkerPos) 129 | else 130 | return name.substring(0, regexMarkerPos) 131 | } 132 | 133 | if (optMarkerPos != -1) 134 | return name.substring(0, optMarkerPos) 135 | 136 | if (regexMarkerPos != -1) 137 | return name.substring(0, regexMarkerPos) 138 | 139 | return name 140 | }, 141 | 142 | segmentIsOptional = function(segment) { 143 | var name = segment.substring(1), 144 | optMarkerPos = name.indexOf('?'), 145 | regexMarkerPos = name.indexOf('|') 146 | 147 | if (optMarkerPos == -1) 148 | return false 149 | 150 | if (regexMarkerPos == -1) 151 | return false 152 | 153 | if (optMarkerPos != -1 && regexMarkerPos != -1) 154 | return optMarkerPos < regexMarkerPos 155 | 156 | return false 157 | }, 158 | 159 | getSegmentDefaultValue = function(segment) { 160 | var name = segment.substring(1), 161 | optMarkerPos = name.indexOf('?'), 162 | regexMarkerPos = name.indexOf('|'), 163 | value = false 164 | 165 | if (optMarkerPos == -1) 166 | return false 167 | 168 | if (regexMarkerPos != -1) 169 | value = segment.substring(optMarkerPos + 1, regexMarkerPos - optMarkerPos - 1) 170 | else 171 | value = segment.substring(optMarkerPos + 1) 172 | 173 | return value ? value : false 174 | }, 175 | 176 | urlFromPattern = function (pattern, params) { 177 | var url = [], 178 | cleanParams = {}, 179 | segments = segmentizeUrl(pattern), 180 | segmentCount = segments.length, 181 | params = params ? params : {}, 182 | paramCount = params.length 183 | 184 | angular.forEach(params, function(value, param) { 185 | var newValue = (param.charAt(0) == ':') 186 | ? param.substring(1) 187 | : param 188 | 189 | cleanParams[param] = value 190 | }) 191 | 192 | params = cleanParams 193 | 194 | angular.forEach(segments, function(segment, index){ 195 | if (segment.charAt(0) != ':') { 196 | url.push(segment) 197 | } 198 | else { 199 | var paramName = getParamName(segment), 200 | optional = segmentIsOptional(segment) 201 | 202 | if (params[paramName]) { 203 | url.push(params[paramName]) 204 | } 205 | else if (optional) { 206 | url.push(getSegmentDefaultValue(segment)) 207 | } 208 | else { 209 | url.push('default') 210 | } 211 | } 212 | }) 213 | 214 | var lastPopulatedIndex = 0 215 | angular.forEach(url, function(segment, index) { 216 | if (segment) 217 | lastPopulatedIndex = index 218 | else 219 | url[index] = 'default' 220 | }) 221 | url = url.slice(0, lastPopulatedIndex + 1) 222 | 223 | return normalizeUrl(url.join('/')) 224 | } 225 | 226 | return { 227 | urlFromPattern: urlFromPattern 228 | } 229 | 230 | }(this.routeConfig) 231 | 232 | this.route = function (routeConfig, helper) { 233 | 234 | pageUrl = function(name, params) { 235 | var pattern = routeConfig.getPagePattern(name), 236 | url = helper.urlFromPattern(pattern, params) 237 | 238 | return routeConfig.getBaseUrl() + url 239 | } 240 | 241 | return { 242 | pageUrl: pageUrl 243 | } 244 | }(this.routeConfig, this.helper) 245 | 246 | 247 | }) 248 | 249 | -------------------------------------------------------------------------------- /assets/vendor/angular/angular-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.4.2 3 | * (c) 2010-2015 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /** 9 | * @ngdoc module 10 | * @name ngRoute 11 | * @description 12 | * 13 | * # ngRoute 14 | * 15 | * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. 16 | * 17 | * ## Example 18 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 19 | * 20 | * 21 | *
22 | */ 23 | /* global -ngRouteModule */ 24 | var ngRouteModule = angular.module('ngRoute', ['ng']). 25 | provider('$route', $RouteProvider), 26 | $routeMinErr = angular.$$minErr('ngRoute'); 27 | 28 | /** 29 | * @ngdoc provider 30 | * @name $routeProvider 31 | * 32 | * @description 33 | * 34 | * Used for configuring routes. 35 | * 36 | * ## Example 37 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 38 | * 39 | * ## Dependencies 40 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 41 | */ 42 | function $RouteProvider() { 43 | function inherit(parent, extra) { 44 | return angular.extend(Object.create(parent), extra); 45 | } 46 | 47 | var routes = {}; 48 | 49 | /** 50 | * @ngdoc method 51 | * @name $routeProvider#when 52 | * 53 | * @param {string} path Route path (matched against `$location.path`). If `$location.path` 54 | * contains redundant trailing slash or is missing one, the route will still match and the 55 | * `$location.path` will be updated to add or drop the trailing slash to exactly match the 56 | * route definition. 57 | * 58 | * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up 59 | * to the next slash are matched and stored in `$routeParams` under the given `name` 60 | * when the route matches. 61 | * * `path` can contain named groups starting with a colon and ending with a star: 62 | * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` 63 | * when the route matches. 64 | * * `path` can contain optional named groups with a question mark: e.g.`:name?`. 65 | * 66 | * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match 67 | * `/color/brown/largecode/code/with/slashes/edit` and extract: 68 | * 69 | * * `color: brown` 70 | * * `largecode: code/with/slashes`. 71 | * 72 | * 73 | * @param {Object} route Mapping information to be assigned to `$route.current` on route 74 | * match. 75 | * 76 | * Object properties: 77 | * 78 | * - `controller` – `{(string|function()=}` – Controller fn that should be associated with 79 | * newly created scope or the name of a {@link angular.Module#controller registered 80 | * controller} if passed as a string. 81 | * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. 82 | * If present, the controller will be published to scope under the `controllerAs` name. 83 | * - `template` – `{string=|function()=}` – html template as a string or a function that 84 | * returns an html template as a string which should be used by {@link 85 | * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. 86 | * This property takes precedence over `templateUrl`. 87 | * 88 | * If `template` is a function, it will be called with the following parameters: 89 | * 90 | * - `{Array.}` - route parameters extracted from the current 91 | * `$location.path()` by applying the current route 92 | * 93 | * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html 94 | * template that should be used by {@link ngRoute.directive:ngView ngView}. 95 | * 96 | * If `templateUrl` is a function, it will be called with the following parameters: 97 | * 98 | * - `{Array.}` - route parameters extracted from the current 99 | * `$location.path()` by applying the current route 100 | * 101 | * - `resolve` - `{Object.=}` - An optional map of dependencies which should 102 | * be injected into the controller. If any of these dependencies are promises, the router 103 | * will wait for them all to be resolved or one to be rejected before the controller is 104 | * instantiated. 105 | * If all the promises are resolved successfully, the values of the resolved promises are 106 | * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is 107 | * fired. If any of the promises are rejected the 108 | * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object 109 | * is: 110 | * 111 | * - `key` – `{string}`: a name of a dependency to be injected into the controller. 112 | * - `factory` - `{string|function}`: If `string` then it is an alias for a service. 113 | * Otherwise if function, then it is {@link auto.$injector#invoke injected} 114 | * and the return value is treated as the dependency. If the result is a promise, it is 115 | * resolved before its value is injected into the controller. Be aware that 116 | * `ngRoute.$routeParams` will still refer to the previous route within these resolve 117 | * functions. Use `$route.current.params` to access the new route parameters, instead. 118 | * 119 | * - `redirectTo` – {(string|function())=} – value to update 120 | * {@link ng.$location $location} path with and trigger route redirection. 121 | * 122 | * If `redirectTo` is a function, it will be called with the following parameters: 123 | * 124 | * - `{Object.}` - route parameters extracted from the current 125 | * `$location.path()` by applying the current route templateUrl. 126 | * - `{string}` - current `$location.path()` 127 | * - `{Object}` - current `$location.search()` 128 | * 129 | * The custom `redirectTo` function is expected to return a string which will be used 130 | * to update `$location.path()` and `$location.search()`. 131 | * 132 | * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` 133 | * or `$location.hash()` changes. 134 | * 135 | * If the option is set to `false` and url in the browser changes, then 136 | * `$routeUpdate` event is broadcasted on the root scope. 137 | * 138 | * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive 139 | * 140 | * If the option is set to `true`, then the particular route can be matched without being 141 | * case sensitive 142 | * 143 | * @returns {Object} self 144 | * 145 | * @description 146 | * Adds a new route definition to the `$route` service. 147 | */ 148 | this.when = function(path, route) { 149 | //copy original route object to preserve params inherited from proto chain 150 | var routeCopy = angular.copy(route); 151 | if (angular.isUndefined(routeCopy.reloadOnSearch)) { 152 | routeCopy.reloadOnSearch = true; 153 | } 154 | if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { 155 | routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; 156 | } 157 | routes[path] = angular.extend( 158 | routeCopy, 159 | path && pathRegExp(path, routeCopy) 160 | ); 161 | 162 | // create redirection for trailing slashes 163 | if (path) { 164 | var redirectPath = (path[path.length - 1] == '/') 165 | ? path.substr(0, path.length - 1) 166 | : path + '/'; 167 | 168 | routes[redirectPath] = angular.extend( 169 | {redirectTo: path}, 170 | pathRegExp(redirectPath, routeCopy) 171 | ); 172 | } 173 | 174 | return this; 175 | }; 176 | 177 | /** 178 | * @ngdoc property 179 | * @name $routeProvider#caseInsensitiveMatch 180 | * @description 181 | * 182 | * A boolean property indicating if routes defined 183 | * using this provider should be matched using a case insensitive 184 | * algorithm. Defaults to `false`. 185 | */ 186 | this.caseInsensitiveMatch = false; 187 | 188 | /** 189 | * @param path {string} path 190 | * @param opts {Object} options 191 | * @return {?Object} 192 | * 193 | * @description 194 | * Normalizes the given path, returning a regular expression 195 | * and the original path. 196 | * 197 | * Inspired by pathRexp in visionmedia/express/lib/utils.js. 198 | */ 199 | function pathRegExp(path, opts) { 200 | var insensitive = opts.caseInsensitiveMatch, 201 | ret = { 202 | originalPath: path, 203 | regexp: path 204 | }, 205 | keys = ret.keys = []; 206 | 207 | path = path 208 | .replace(/([().])/g, '\\$1') 209 | .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { 210 | var optional = option === '?' ? option : null; 211 | var star = option === '*' ? option : null; 212 | keys.push({ name: key, optional: !!optional }); 213 | slash = slash || ''; 214 | return '' 215 | + (optional ? '' : slash) 216 | + '(?:' 217 | + (optional ? slash : '') 218 | + (star && '(.+?)' || '([^/]+)') 219 | + (optional || '') 220 | + ')' 221 | + (optional || ''); 222 | }) 223 | .replace(/([\/$\*])/g, '\\$1'); 224 | 225 | ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); 226 | return ret; 227 | } 228 | 229 | /** 230 | * @ngdoc method 231 | * @name $routeProvider#otherwise 232 | * 233 | * @description 234 | * Sets route definition that will be used on route change when no other route definition 235 | * is matched. 236 | * 237 | * @param {Object|string} params Mapping information to be assigned to `$route.current`. 238 | * If called with a string, the value maps to `redirectTo`. 239 | * @returns {Object} self 240 | */ 241 | this.otherwise = function(params) { 242 | if (typeof params === 'string') { 243 | params = {redirectTo: params}; 244 | } 245 | this.when(null, params); 246 | return this; 247 | }; 248 | 249 | 250 | this.$get = ['$rootScope', 251 | '$location', 252 | '$routeParams', 253 | '$q', 254 | '$injector', 255 | '$templateRequest', 256 | '$sce', 257 | function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { 258 | 259 | /** 260 | * @ngdoc service 261 | * @name $route 262 | * @requires $location 263 | * @requires $routeParams 264 | * 265 | * @property {Object} current Reference to the current route definition. 266 | * The route definition contains: 267 | * 268 | * - `controller`: The controller constructor as define in route definition. 269 | * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for 270 | * controller instantiation. The `locals` contain 271 | * the resolved values of the `resolve` map. Additionally the `locals` also contain: 272 | * 273 | * - `$scope` - The current route scope. 274 | * - `$template` - The current route template HTML. 275 | * 276 | * @property {Object} routes Object with all route configuration Objects as its properties. 277 | * 278 | * @description 279 | * `$route` is used for deep-linking URLs to controllers and views (HTML partials). 280 | * It watches `$location.url()` and tries to map the path to an existing route definition. 281 | * 282 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 283 | * 284 | * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. 285 | * 286 | * The `$route` service is typically used in conjunction with the 287 | * {@link ngRoute.directive:ngView `ngView`} directive and the 288 | * {@link ngRoute.$routeParams `$routeParams`} service. 289 | * 290 | * @example 291 | * This example shows how changing the URL hash causes the `$route` to match a route against the 292 | * URL, and the `ngView` pulls in the partial. 293 | * 294 | * 296 | * 297 | *
298 | * Choose: 299 | * Moby | 300 | * Moby: Ch1 | 301 | * Gatsby | 302 | * Gatsby: Ch4 | 303 | * Scarlet Letter
304 | * 305 | *
306 | * 307 | *
308 | * 309 | *
$location.path() = {{$location.path()}}
310 | *
$route.current.templateUrl = {{$route.current.templateUrl}}
311 | *
$route.current.params = {{$route.current.params}}
312 | *
$route.current.scope.name = {{$route.current.scope.name}}
313 | *
$routeParams = {{$routeParams}}
314 | *
315 | *
316 | * 317 | * 318 | * controller: {{name}}
319 | * Book Id: {{params.bookId}}
320 | *
321 | * 322 | * 323 | * controller: {{name}}
324 | * Book Id: {{params.bookId}}
325 | * Chapter Id: {{params.chapterId}} 326 | *
327 | * 328 | * 329 | * angular.module('ngRouteExample', ['ngRoute']) 330 | * 331 | * .controller('MainController', function($scope, $route, $routeParams, $location) { 332 | * $scope.$route = $route; 333 | * $scope.$location = $location; 334 | * $scope.$routeParams = $routeParams; 335 | * }) 336 | * 337 | * .controller('BookController', function($scope, $routeParams) { 338 | * $scope.name = "BookController"; 339 | * $scope.params = $routeParams; 340 | * }) 341 | * 342 | * .controller('ChapterController', function($scope, $routeParams) { 343 | * $scope.name = "ChapterController"; 344 | * $scope.params = $routeParams; 345 | * }) 346 | * 347 | * .config(function($routeProvider, $locationProvider) { 348 | * $routeProvider 349 | * .when('/Book/:bookId', { 350 | * templateUrl: 'book.html', 351 | * controller: 'BookController', 352 | * resolve: { 353 | * // I will cause a 1 second delay 354 | * delay: function($q, $timeout) { 355 | * var delay = $q.defer(); 356 | * $timeout(delay.resolve, 1000); 357 | * return delay.promise; 358 | * } 359 | * } 360 | * }) 361 | * .when('/Book/:bookId/ch/:chapterId', { 362 | * templateUrl: 'chapter.html', 363 | * controller: 'ChapterController' 364 | * }); 365 | * 366 | * // configure html5 to get links working on jsfiddle 367 | * $locationProvider.html5Mode(true); 368 | * }); 369 | * 370 | * 371 | * 372 | * 373 | * it('should load and compile correct template', function() { 374 | * element(by.linkText('Moby: Ch1')).click(); 375 | * var content = element(by.css('[ng-view]')).getText(); 376 | * expect(content).toMatch(/controller\: ChapterController/); 377 | * expect(content).toMatch(/Book Id\: Moby/); 378 | * expect(content).toMatch(/Chapter Id\: 1/); 379 | * 380 | * element(by.partialLinkText('Scarlet')).click(); 381 | * 382 | * content = element(by.css('[ng-view]')).getText(); 383 | * expect(content).toMatch(/controller\: BookController/); 384 | * expect(content).toMatch(/Book Id\: Scarlet/); 385 | * }); 386 | * 387 | *
388 | */ 389 | 390 | /** 391 | * @ngdoc event 392 | * @name $route#$routeChangeStart 393 | * @eventType broadcast on root scope 394 | * @description 395 | * Broadcasted before a route change. At this point the route services starts 396 | * resolving all of the dependencies needed for the route change to occur. 397 | * Typically this involves fetching the view template as well as any dependencies 398 | * defined in `resolve` route property. Once all of the dependencies are resolved 399 | * `$routeChangeSuccess` is fired. 400 | * 401 | * The route change (and the `$location` change that triggered it) can be prevented 402 | * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} 403 | * for more details about event object. 404 | * 405 | * @param {Object} angularEvent Synthetic event object. 406 | * @param {Route} next Future route information. 407 | * @param {Route} current Current route information. 408 | */ 409 | 410 | /** 411 | * @ngdoc event 412 | * @name $route#$routeChangeSuccess 413 | * @eventType broadcast on root scope 414 | * @description 415 | * Broadcasted after a route dependencies are resolved. 416 | * {@link ngRoute.directive:ngView ngView} listens for the directive 417 | * to instantiate the controller and render the view. 418 | * 419 | * @param {Object} angularEvent Synthetic event object. 420 | * @param {Route} current Current route information. 421 | * @param {Route|Undefined} previous Previous route information, or undefined if current is 422 | * first route entered. 423 | */ 424 | 425 | /** 426 | * @ngdoc event 427 | * @name $route#$routeChangeError 428 | * @eventType broadcast on root scope 429 | * @description 430 | * Broadcasted if any of the resolve promises are rejected. 431 | * 432 | * @param {Object} angularEvent Synthetic event object 433 | * @param {Route} current Current route information. 434 | * @param {Route} previous Previous route information. 435 | * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. 436 | */ 437 | 438 | /** 439 | * @ngdoc event 440 | * @name $route#$routeUpdate 441 | * @eventType broadcast on root scope 442 | * @description 443 | * The `reloadOnSearch` property has been set to false, and we are reusing the same 444 | * instance of the Controller. 445 | * 446 | * @param {Object} angularEvent Synthetic event object 447 | * @param {Route} current Current/previous route information. 448 | */ 449 | 450 | var forceReload = false, 451 | preparedRoute, 452 | preparedRouteIsUpdateOnly, 453 | $route = { 454 | routes: routes, 455 | 456 | /** 457 | * @ngdoc method 458 | * @name $route#reload 459 | * 460 | * @description 461 | * Causes `$route` service to reload the current route even if 462 | * {@link ng.$location $location} hasn't changed. 463 | * 464 | * As a result of that, {@link ngRoute.directive:ngView ngView} 465 | * creates new scope and reinstantiates the controller. 466 | */ 467 | reload: function() { 468 | forceReload = true; 469 | $rootScope.$evalAsync(function() { 470 | // Don't support cancellation of a reload for now... 471 | prepareRoute(); 472 | commitRoute(); 473 | }); 474 | }, 475 | 476 | /** 477 | * @ngdoc method 478 | * @name $route#updateParams 479 | * 480 | * @description 481 | * Causes `$route` service to update the current URL, replacing 482 | * current route parameters with those specified in `newParams`. 483 | * Provided property names that match the route's path segment 484 | * definitions will be interpolated into the location's path, while 485 | * remaining properties will be treated as query params. 486 | * 487 | * @param {!Object} newParams mapping of URL parameter names to values 488 | */ 489 | updateParams: function(newParams) { 490 | if (this.current && this.current.$$route) { 491 | newParams = angular.extend({}, this.current.params, newParams); 492 | $location.path(interpolate(this.current.$$route.originalPath, newParams)); 493 | // interpolate modifies newParams, only query params are left 494 | $location.search(newParams); 495 | } else { 496 | throw $routeMinErr('norout', 'Tried updating route when with no current route'); 497 | } 498 | } 499 | }; 500 | 501 | $rootScope.$on('$locationChangeStart', prepareRoute); 502 | $rootScope.$on('$locationChangeSuccess', commitRoute); 503 | 504 | return $route; 505 | 506 | ///////////////////////////////////////////////////// 507 | 508 | /** 509 | * @param on {string} current url 510 | * @param route {Object} route regexp to match the url against 511 | * @return {?Object} 512 | * 513 | * @description 514 | * Check if the route matches the current url. 515 | * 516 | * Inspired by match in 517 | * visionmedia/express/lib/router/router.js. 518 | */ 519 | function switchRouteMatcher(on, route) { 520 | var keys = route.keys, 521 | params = {}; 522 | 523 | if (!route.regexp) return null; 524 | 525 | var m = route.regexp.exec(on); 526 | if (!m) return null; 527 | 528 | for (var i = 1, len = m.length; i < len; ++i) { 529 | var key = keys[i - 1]; 530 | 531 | var val = m[i]; 532 | 533 | if (key && val) { 534 | params[key.name] = val; 535 | } 536 | } 537 | return params; 538 | } 539 | 540 | function prepareRoute($locationEvent) { 541 | var lastRoute = $route.current; 542 | 543 | preparedRoute = parseRoute(); 544 | preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route 545 | && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) 546 | && !preparedRoute.reloadOnSearch && !forceReload; 547 | 548 | if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { 549 | if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { 550 | if ($locationEvent) { 551 | $locationEvent.preventDefault(); 552 | } 553 | } 554 | } 555 | } 556 | 557 | function commitRoute() { 558 | var lastRoute = $route.current; 559 | var nextRoute = preparedRoute; 560 | 561 | if (preparedRouteIsUpdateOnly) { 562 | lastRoute.params = nextRoute.params; 563 | angular.copy(lastRoute.params, $routeParams); 564 | $rootScope.$broadcast('$routeUpdate', lastRoute); 565 | } else if (nextRoute || lastRoute) { 566 | forceReload = false; 567 | $route.current = nextRoute; 568 | if (nextRoute) { 569 | if (nextRoute.redirectTo) { 570 | if (angular.isString(nextRoute.redirectTo)) { 571 | $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) 572 | .replace(); 573 | } else { 574 | $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) 575 | .replace(); 576 | } 577 | } 578 | } 579 | 580 | $q.when(nextRoute). 581 | then(function() { 582 | if (nextRoute) { 583 | var locals = angular.extend({}, nextRoute.resolve), 584 | template, templateUrl; 585 | 586 | angular.forEach(locals, function(value, key) { 587 | locals[key] = angular.isString(value) ? 588 | $injector.get(value) : $injector.invoke(value, null, null, key); 589 | }); 590 | 591 | if (angular.isDefined(template = nextRoute.template)) { 592 | if (angular.isFunction(template)) { 593 | template = template(nextRoute.params); 594 | } 595 | } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { 596 | if (angular.isFunction(templateUrl)) { 597 | templateUrl = templateUrl(nextRoute.params); 598 | } 599 | if (angular.isDefined(templateUrl)) { 600 | nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl); 601 | template = $templateRequest(templateUrl); 602 | } 603 | } 604 | if (angular.isDefined(template)) { 605 | locals['$template'] = template; 606 | } 607 | return $q.all(locals); 608 | } 609 | }). 610 | then(function(locals) { 611 | // after route change 612 | if (nextRoute == $route.current) { 613 | if (nextRoute) { 614 | nextRoute.locals = locals; 615 | angular.copy(nextRoute.params, $routeParams); 616 | } 617 | $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); 618 | } 619 | }, function(error) { 620 | if (nextRoute == $route.current) { 621 | $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); 622 | } 623 | }); 624 | } 625 | } 626 | 627 | 628 | /** 629 | * @returns {Object} the current active route, by matching it against the URL 630 | */ 631 | function parseRoute() { 632 | // Match a route 633 | var params, match; 634 | angular.forEach(routes, function(route, path) { 635 | if (!match && (params = switchRouteMatcher($location.path(), route))) { 636 | match = inherit(route, { 637 | params: angular.extend({}, $location.search(), params), 638 | pathParams: params}); 639 | match.$$route = route; 640 | } 641 | }); 642 | // No route matched; fallback to "otherwise" route 643 | return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); 644 | } 645 | 646 | /** 647 | * @returns {string} interpolation of the redirect path with the parameters 648 | */ 649 | function interpolate(string, params) { 650 | var result = []; 651 | angular.forEach((string || '').split(':'), function(segment, i) { 652 | if (i === 0) { 653 | result.push(segment); 654 | } else { 655 | var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); 656 | var key = segmentMatch[1]; 657 | result.push(params[key]); 658 | result.push(segmentMatch[2] || ''); 659 | delete params[key]; 660 | } 661 | }); 662 | return result.join(''); 663 | } 664 | }]; 665 | } 666 | 667 | ngRouteModule.provider('$routeParams', $RouteParamsProvider); 668 | 669 | 670 | /** 671 | * @ngdoc service 672 | * @name $routeParams 673 | * @requires $route 674 | * 675 | * @description 676 | * The `$routeParams` service allows you to retrieve the current set of route parameters. 677 | * 678 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 679 | * 680 | * The route parameters are a combination of {@link ng.$location `$location`}'s 681 | * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. 682 | * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. 683 | * 684 | * In case of parameter name collision, `path` params take precedence over `search` params. 685 | * 686 | * The service guarantees that the identity of the `$routeParams` object will remain unchanged 687 | * (but its properties will likely change) even when a route change occurs. 688 | * 689 | * Note that the `$routeParams` are only updated *after* a route change completes successfully. 690 | * This means that you cannot rely on `$routeParams` being correct in route resolve functions. 691 | * Instead you can use `$route.current.params` to access the new route's parameters. 692 | * 693 | * @example 694 | * ```js 695 | * // Given: 696 | * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby 697 | * // Route: /Chapter/:chapterId/Section/:sectionId 698 | * // 699 | * // Then 700 | * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} 701 | * ``` 702 | */ 703 | function $RouteParamsProvider() { 704 | this.$get = function() { return {}; }; 705 | } 706 | 707 | ngRouteModule.directive('ngView', ngViewFactory); 708 | ngRouteModule.directive('ngView', ngViewFillContentFactory); 709 | 710 | 711 | /** 712 | * @ngdoc directive 713 | * @name ngView 714 | * @restrict ECA 715 | * 716 | * @description 717 | * # Overview 718 | * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by 719 | * including the rendered template of the current route into the main layout (`index.html`) file. 720 | * Every time the current route changes, the included view changes with it according to the 721 | * configuration of the `$route` service. 722 | * 723 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 724 | * 725 | * @animations 726 | * enter - animation is used to bring new content into the browser. 727 | * leave - animation is used to animate existing content away. 728 | * 729 | * The enter and leave animation occur concurrently. 730 | * 731 | * @scope 732 | * @priority 400 733 | * @param {string=} onload Expression to evaluate whenever the view updates. 734 | * 735 | * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll 736 | * $anchorScroll} to scroll the viewport after the view is updated. 737 | * 738 | * - If the attribute is not set, disable scrolling. 739 | * - If the attribute is set without value, enable scrolling. 740 | * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated 741 | * as an expression yields a truthy value. 742 | * @example 743 | 746 | 747 |
748 | Choose: 749 | Moby | 750 | Moby: Ch1 | 751 | Gatsby | 752 | Gatsby: Ch4 | 753 | Scarlet Letter
754 | 755 |
756 |
757 |
758 |
759 | 760 |
$location.path() = {{main.$location.path()}}
761 |
$route.current.templateUrl = {{main.$route.current.templateUrl}}
762 |
$route.current.params = {{main.$route.current.params}}
763 |
$routeParams = {{main.$routeParams}}
764 |
765 |
766 | 767 | 768 |
769 | controller: {{book.name}}
770 | Book Id: {{book.params.bookId}}
771 |
772 |
773 | 774 | 775 |
776 | controller: {{chapter.name}}
777 | Book Id: {{chapter.params.bookId}}
778 | Chapter Id: {{chapter.params.chapterId}} 779 |
780 |
781 | 782 | 783 | .view-animate-container { 784 | position:relative; 785 | height:100px!important; 786 | background:white; 787 | border:1px solid black; 788 | height:40px; 789 | overflow:hidden; 790 | } 791 | 792 | .view-animate { 793 | padding:10px; 794 | } 795 | 796 | .view-animate.ng-enter, .view-animate.ng-leave { 797 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 798 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 799 | 800 | display:block; 801 | width:100%; 802 | border-left:1px solid black; 803 | 804 | position:absolute; 805 | top:0; 806 | left:0; 807 | right:0; 808 | bottom:0; 809 | padding:10px; 810 | } 811 | 812 | .view-animate.ng-enter { 813 | left:100%; 814 | } 815 | .view-animate.ng-enter.ng-enter-active { 816 | left:0; 817 | } 818 | .view-animate.ng-leave.ng-leave-active { 819 | left:-100%; 820 | } 821 | 822 | 823 | 824 | angular.module('ngViewExample', ['ngRoute', 'ngAnimate']) 825 | .config(['$routeProvider', '$locationProvider', 826 | function($routeProvider, $locationProvider) { 827 | $routeProvider 828 | .when('/Book/:bookId', { 829 | templateUrl: 'book.html', 830 | controller: 'BookCtrl', 831 | controllerAs: 'book' 832 | }) 833 | .when('/Book/:bookId/ch/:chapterId', { 834 | templateUrl: 'chapter.html', 835 | controller: 'ChapterCtrl', 836 | controllerAs: 'chapter' 837 | }); 838 | 839 | $locationProvider.html5Mode(true); 840 | }]) 841 | .controller('MainCtrl', ['$route', '$routeParams', '$location', 842 | function($route, $routeParams, $location) { 843 | this.$route = $route; 844 | this.$location = $location; 845 | this.$routeParams = $routeParams; 846 | }]) 847 | .controller('BookCtrl', ['$routeParams', function($routeParams) { 848 | this.name = "BookCtrl"; 849 | this.params = $routeParams; 850 | }]) 851 | .controller('ChapterCtrl', ['$routeParams', function($routeParams) { 852 | this.name = "ChapterCtrl"; 853 | this.params = $routeParams; 854 | }]); 855 | 856 | 857 | 858 | 859 | it('should load and compile correct template', function() { 860 | element(by.linkText('Moby: Ch1')).click(); 861 | var content = element(by.css('[ng-view]')).getText(); 862 | expect(content).toMatch(/controller\: ChapterCtrl/); 863 | expect(content).toMatch(/Book Id\: Moby/); 864 | expect(content).toMatch(/Chapter Id\: 1/); 865 | 866 | element(by.partialLinkText('Scarlet')).click(); 867 | 868 | content = element(by.css('[ng-view]')).getText(); 869 | expect(content).toMatch(/controller\: BookCtrl/); 870 | expect(content).toMatch(/Book Id\: Scarlet/); 871 | }); 872 | 873 |
874 | */ 875 | 876 | 877 | /** 878 | * @ngdoc event 879 | * @name ngView#$viewContentLoaded 880 | * @eventType emit on the current ngView scope 881 | * @description 882 | * Emitted every time the ngView content is reloaded. 883 | */ 884 | ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; 885 | function ngViewFactory($route, $anchorScroll, $animate) { 886 | return { 887 | restrict: 'ECA', 888 | terminal: true, 889 | priority: 400, 890 | transclude: 'element', 891 | link: function(scope, $element, attr, ctrl, $transclude) { 892 | var currentScope, 893 | currentElement, 894 | previousLeaveAnimation, 895 | autoScrollExp = attr.autoscroll, 896 | onloadExp = attr.onload || ''; 897 | 898 | scope.$on('$routeChangeSuccess', update); 899 | update(); 900 | 901 | function cleanupLastView() { 902 | if (previousLeaveAnimation) { 903 | $animate.cancel(previousLeaveAnimation); 904 | previousLeaveAnimation = null; 905 | } 906 | 907 | if (currentScope) { 908 | currentScope.$destroy(); 909 | currentScope = null; 910 | } 911 | if (currentElement) { 912 | previousLeaveAnimation = $animate.leave(currentElement); 913 | previousLeaveAnimation.then(function() { 914 | previousLeaveAnimation = null; 915 | }); 916 | currentElement = null; 917 | } 918 | } 919 | 920 | function update() { 921 | var locals = $route.current && $route.current.locals, 922 | template = locals && locals.$template; 923 | 924 | if (angular.isDefined(template)) { 925 | var newScope = scope.$new(); 926 | var current = $route.current; 927 | 928 | // Note: This will also link all children of ng-view that were contained in the original 929 | // html. If that content contains controllers, ... they could pollute/change the scope. 930 | // However, using ng-view on an element with additional content does not make sense... 931 | // Note: We can't remove them in the cloneAttchFn of $transclude as that 932 | // function is called before linking the content, which would apply child 933 | // directives to non existing elements. 934 | var clone = $transclude(newScope, function(clone) { 935 | $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { 936 | if (angular.isDefined(autoScrollExp) 937 | && (!autoScrollExp || scope.$eval(autoScrollExp))) { 938 | $anchorScroll(); 939 | } 940 | }); 941 | cleanupLastView(); 942 | }); 943 | 944 | currentElement = clone; 945 | currentScope = current.scope = newScope; 946 | currentScope.$emit('$viewContentLoaded'); 947 | currentScope.$eval(onloadExp); 948 | } else { 949 | cleanupLastView(); 950 | } 951 | } 952 | } 953 | }; 954 | } 955 | 956 | // This directive is called during the $transclude call of the first `ngView` directive. 957 | // It will replace and compile the content of the element with the loaded template. 958 | // We need this directive so that the element content is already filled when 959 | // the link function of another directive on the same element as ngView 960 | // is called. 961 | ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; 962 | function ngViewFillContentFactory($compile, $controller, $route) { 963 | return { 964 | restrict: 'ECA', 965 | priority: -400, 966 | link: function(scope, $element) { 967 | var current = $route.current, 968 | locals = current.locals; 969 | 970 | $element.html(locals.$template); 971 | 972 | var link = $compile($element.contents()); 973 | 974 | if (current.controller) { 975 | locals.$scope = scope; 976 | var controller = $controller(current.controller, locals); 977 | if (current.controllerAs) { 978 | scope[current.controllerAs] = controller; 979 | } 980 | $element.data('$ngControllerController', controller); 981 | $element.children().data('$ngControllerController', controller); 982 | } 983 | 984 | link(scope); 985 | } 986 | }; 987 | } 988 | 989 | 990 | })(window, window.angular); 991 | -------------------------------------------------------------------------------- /assets/vendor/angular/angular-sanitize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.4.2 3 | * (c) 2010-2015 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) {'use strict'; 7 | 8 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 9 | * Any commits to this file should be reviewed with security in mind. * 10 | * Changes to this file can potentially create security vulnerabilities. * 11 | * An approval from 2 Core members with history of modifying * 12 | * this file is required. * 13 | * * 14 | * Does the change somehow allow for arbitrary javascript to be executed? * 15 | * Or allows for someone to change the prototype of built-in objects? * 16 | * Or gives undesired access to variables likes document or window? * 17 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 18 | 19 | var $sanitizeMinErr = angular.$$minErr('$sanitize'); 20 | 21 | /** 22 | * @ngdoc module 23 | * @name ngSanitize 24 | * @description 25 | * 26 | * # ngSanitize 27 | * 28 | * The `ngSanitize` module provides functionality to sanitize HTML. 29 | * 30 | * 31 | *
32 | * 33 | * See {@link ngSanitize.$sanitize `$sanitize`} for usage. 34 | */ 35 | 36 | /* 37 | * HTML Parser By Misko Hevery (misko@hevery.com) 38 | * based on: HTML Parser By John Resig (ejohn.org) 39 | * Original code by Erik Arvidsson, Mozilla Public License 40 | * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 41 | * 42 | * // Use like so: 43 | * htmlParser(htmlString, { 44 | * start: function(tag, attrs, unary) {}, 45 | * end: function(tag) {}, 46 | * chars: function(text) {}, 47 | * comment: function(text) {} 48 | * }); 49 | * 50 | */ 51 | 52 | 53 | /** 54 | * @ngdoc service 55 | * @name $sanitize 56 | * @kind function 57 | * 58 | * @description 59 | * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are 60 | * then serialized back to properly escaped html string. This means that no unsafe input can make 61 | * it into the returned string, however, since our parser is more strict than a typical browser 62 | * parser, it's possible that some obscure input, which would be recognized as valid HTML by a 63 | * browser, won't make it through the sanitizer. The input may also contain SVG markup. 64 | * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and 65 | * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. 66 | * 67 | * @param {string} html HTML input. 68 | * @returns {string} Sanitized HTML. 69 | * 70 | * @example 71 | 72 | 73 | 85 |
86 | Snippet: 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value 104 |
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
105 | </div>
106 |
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
116 |
117 |
118 | 119 | it('should sanitize the html snippet by default', function() { 120 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 121 | toBe('

an html\nclick here\nsnippet

'); 122 | }); 123 | 124 | it('should inline raw snippet if bound to a trusted value', function() { 125 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). 126 | toBe("

an html\n" + 127 | "click here\n" + 128 | "snippet

"); 129 | }); 130 | 131 | it('should escape snippet without any filter', function() { 132 | expect(element(by.css('#bind-default div')).getInnerHtml()). 133 | toBe("<p style=\"color:blue\">an html\n" + 134 | "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 135 | "snippet</p>"); 136 | }); 137 | 138 | it('should update', function() { 139 | element(by.model('snippet')).clear(); 140 | element(by.model('snippet')).sendKeys('new text'); 141 | expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). 142 | toBe('new text'); 143 | expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( 144 | 'new text'); 145 | expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( 146 | "new <b onclick=\"alert(1)\">text</b>"); 147 | }); 148 |
149 |
150 | */ 151 | function $SanitizeProvider() { 152 | this.$get = ['$$sanitizeUri', function($$sanitizeUri) { 153 | return function(html) { 154 | var buf = []; 155 | htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { 156 | return !/^unsafe/.test($$sanitizeUri(uri, isImage)); 157 | })); 158 | return buf.join(''); 159 | }; 160 | }]; 161 | } 162 | 163 | function sanitizeText(chars) { 164 | var buf = []; 165 | var writer = htmlSanitizeWriter(buf, angular.noop); 166 | writer.chars(chars); 167 | return buf.join(''); 168 | } 169 | 170 | 171 | // Regular Expressions for parsing tags and attributes 172 | var START_TAG_REGEXP = 173 | /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, 174 | END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, 175 | ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, 176 | BEGIN_TAG_REGEXP = /^/g, 179 | DOCTYPE_REGEXP = /]*?)>/i, 180 | CDATA_REGEXP = //g, 181 | SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 182 | // Match everything outside of normal chars and " (quote character) 183 | NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; 184 | 185 | 186 | // Good source of info about elements and attributes 187 | // http://dev.w3.org/html5/spec/Overview.html#semantics 188 | // http://simon.html5.org/html-elements 189 | 190 | // Safe Void Elements - HTML5 191 | // http://dev.w3.org/html5/spec/Overview.html#void-elements 192 | var voidElements = makeMap("area,br,col,hr,img,wbr"); 193 | 194 | // Elements that you can, intentionally, leave open (and which close themselves) 195 | // http://dev.w3.org/html5/spec/Overview.html#optional-tags 196 | var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), 197 | optionalEndTagInlineElements = makeMap("rp,rt"), 198 | optionalEndTagElements = angular.extend({}, 199 | optionalEndTagInlineElements, 200 | optionalEndTagBlockElements); 201 | 202 | // Safe Block Elements - HTML5 203 | var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + 204 | "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + 205 | "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); 206 | 207 | // Inline Elements - HTML5 208 | var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + 209 | "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + 210 | "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); 211 | 212 | // SVG Elements 213 | // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements 214 | // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. 215 | // They can potentially allow for arbitrary javascript to be executed. See #11290 216 | var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + 217 | "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + 218 | "radialGradient,rect,stop,svg,switch,text,title,tspan,use"); 219 | 220 | // Special Elements (can contain anything) 221 | var specialElements = makeMap("script,style"); 222 | 223 | var validElements = angular.extend({}, 224 | voidElements, 225 | blockElements, 226 | inlineElements, 227 | optionalEndTagElements, 228 | svgElements); 229 | 230 | //Attributes that have href and hence need to be sanitized 231 | var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); 232 | 233 | var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + 234 | 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + 235 | 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + 236 | 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + 237 | 'valign,value,vspace,width'); 238 | 239 | // SVG attributes (without "id" and "name" attributes) 240 | // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes 241 | var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + 242 | 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + 243 | 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + 244 | 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + 245 | 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + 246 | 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + 247 | 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + 248 | 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + 249 | 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + 250 | 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + 251 | 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + 252 | 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + 253 | 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + 254 | 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + 255 | 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); 256 | 257 | var validAttrs = angular.extend({}, 258 | uriAttrs, 259 | svgAttrs, 260 | htmlAttrs); 261 | 262 | function makeMap(str, lowercaseKeys) { 263 | var obj = {}, items = str.split(','), i; 264 | for (i = 0; i < items.length; i++) { 265 | obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; 266 | } 267 | return obj; 268 | } 269 | 270 | 271 | /** 272 | * @example 273 | * htmlParser(htmlString, { 274 | * start: function(tag, attrs, unary) {}, 275 | * end: function(tag) {}, 276 | * chars: function(text) {}, 277 | * comment: function(text) {} 278 | * }); 279 | * 280 | * @param {string} html string 281 | * @param {object} handler 282 | */ 283 | function htmlParser(html, handler) { 284 | if (typeof html !== 'string') { 285 | if (html === null || typeof html === 'undefined') { 286 | html = ''; 287 | } else { 288 | html = '' + html; 289 | } 290 | } 291 | var index, chars, match, stack = [], last = html, text; 292 | stack.last = function() { return stack[stack.length - 1]; }; 293 | 294 | while (html) { 295 | text = ''; 296 | chars = true; 297 | 298 | // Make sure we're not in a script or style element 299 | if (!stack.last() || !specialElements[stack.last()]) { 300 | 301 | // Comment 302 | if (html.indexOf("", index) === index) { 307 | if (handler.comment) handler.comment(html.substring(4, index)); 308 | html = html.substring(index + 3); 309 | chars = false; 310 | } 311 | // DOCTYPE 312 | } else if (DOCTYPE_REGEXP.test(html)) { 313 | match = html.match(DOCTYPE_REGEXP); 314 | 315 | if (match) { 316 | html = html.replace(match[0], ''); 317 | chars = false; 318 | } 319 | // end tag 320 | } else if (BEGING_END_TAGE_REGEXP.test(html)) { 321 | match = html.match(END_TAG_REGEXP); 322 | 323 | if (match) { 324 | html = html.substring(match[0].length); 325 | match[0].replace(END_TAG_REGEXP, parseEndTag); 326 | chars = false; 327 | } 328 | 329 | // start tag 330 | } else if (BEGIN_TAG_REGEXP.test(html)) { 331 | match = html.match(START_TAG_REGEXP); 332 | 333 | if (match) { 334 | // We only have a valid start-tag if there is a '>'. 335 | if (match[4]) { 336 | html = html.substring(match[0].length); 337 | match[0].replace(START_TAG_REGEXP, parseStartTag); 338 | } 339 | chars = false; 340 | } else { 341 | // no ending tag found --- this piece should be encoded as an entity. 342 | text += '<'; 343 | html = html.substring(1); 344 | } 345 | } 346 | 347 | if (chars) { 348 | index = html.indexOf("<"); 349 | 350 | text += index < 0 ? html : html.substring(0, index); 351 | html = index < 0 ? "" : html.substring(index); 352 | 353 | if (handler.chars) handler.chars(decodeEntities(text)); 354 | } 355 | 356 | } else { 357 | // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w]. 358 | html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), 359 | function(all, text) { 360 | text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); 361 | 362 | if (handler.chars) handler.chars(decodeEntities(text)); 363 | 364 | return ""; 365 | }); 366 | 367 | parseEndTag("", stack.last()); 368 | } 369 | 370 | if (html == last) { 371 | throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + 372 | "of html: {0}", html); 373 | } 374 | last = html; 375 | } 376 | 377 | // Clean up any remaining tags 378 | parseEndTag(); 379 | 380 | function parseStartTag(tag, tagName, rest, unary) { 381 | tagName = angular.lowercase(tagName); 382 | if (blockElements[tagName]) { 383 | while (stack.last() && inlineElements[stack.last()]) { 384 | parseEndTag("", stack.last()); 385 | } 386 | } 387 | 388 | if (optionalEndTagElements[tagName] && stack.last() == tagName) { 389 | parseEndTag("", tagName); 390 | } 391 | 392 | unary = voidElements[tagName] || !!unary; 393 | 394 | if (!unary) { 395 | stack.push(tagName); 396 | } 397 | 398 | var attrs = {}; 399 | 400 | rest.replace(ATTR_REGEXP, 401 | function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { 402 | var value = doubleQuotedValue 403 | || singleQuotedValue 404 | || unquotedValue 405 | || ''; 406 | 407 | attrs[name] = decodeEntities(value); 408 | }); 409 | if (handler.start) handler.start(tagName, attrs, unary); 410 | } 411 | 412 | function parseEndTag(tag, tagName) { 413 | var pos = 0, i; 414 | tagName = angular.lowercase(tagName); 415 | if (tagName) { 416 | // Find the closest opened tag of the same type 417 | for (pos = stack.length - 1; pos >= 0; pos--) { 418 | if (stack[pos] == tagName) break; 419 | } 420 | } 421 | 422 | if (pos >= 0) { 423 | // Close all the open elements, up the stack 424 | for (i = stack.length - 1; i >= pos; i--) 425 | if (handler.end) handler.end(stack[i]); 426 | 427 | // Remove the open elements from the stack 428 | stack.length = pos; 429 | } 430 | } 431 | } 432 | 433 | var hiddenPre=document.createElement("pre"); 434 | /** 435 | * decodes all entities into regular string 436 | * @param value 437 | * @returns {string} A string with decoded entities. 438 | */ 439 | function decodeEntities(value) { 440 | if (!value) { return ''; } 441 | 442 | hiddenPre.innerHTML = value.replace(//g, '>'); 468 | } 469 | 470 | /** 471 | * create an HTML/XML writer which writes to buffer 472 | * @param {Array} buf use buf.jain('') to get out sanitized html string 473 | * @returns {object} in the form of { 474 | * start: function(tag, attrs, unary) {}, 475 | * end: function(tag) {}, 476 | * chars: function(text) {}, 477 | * comment: function(text) {} 478 | * } 479 | */ 480 | function htmlSanitizeWriter(buf, uriValidator) { 481 | var ignore = false; 482 | var out = angular.bind(buf, buf.push); 483 | return { 484 | start: function(tag, attrs, unary) { 485 | tag = angular.lowercase(tag); 486 | if (!ignore && specialElements[tag]) { 487 | ignore = tag; 488 | } 489 | if (!ignore && validElements[tag] === true) { 490 | out('<'); 491 | out(tag); 492 | angular.forEach(attrs, function(value, key) { 493 | var lkey=angular.lowercase(key); 494 | var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); 495 | if (validAttrs[lkey] === true && 496 | (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { 497 | out(' '); 498 | out(key); 499 | out('="'); 500 | out(encodeEntities(value)); 501 | out('"'); 502 | } 503 | }); 504 | out(unary ? '/>' : '>'); 505 | } 506 | }, 507 | end: function(tag) { 508 | tag = angular.lowercase(tag); 509 | if (!ignore && validElements[tag] === true) { 510 | out(''); 513 | } 514 | if (tag == ignore) { 515 | ignore = false; 516 | } 517 | }, 518 | chars: function(chars) { 519 | if (!ignore) { 520 | out(encodeEntities(chars)); 521 | } 522 | } 523 | }; 524 | } 525 | 526 | 527 | // define ngSanitize module and register $sanitize service 528 | angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); 529 | 530 | /* global sanitizeText: false */ 531 | 532 | /** 533 | * @ngdoc filter 534 | * @name linky 535 | * @kind function 536 | * 537 | * @description 538 | * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and 539 | * plain email address links. 540 | * 541 | * Requires the {@link ngSanitize `ngSanitize`} module to be installed. 542 | * 543 | * @param {string} text Input text. 544 | * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. 545 | * @returns {string} Html-linkified text. 546 | * 547 | * @usage 548 | 549 | * 550 | * @example 551 | 552 | 553 | 565 |
566 | Snippet: 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 578 | 581 | 582 | 583 | 584 | 587 | 590 | 591 | 592 | 593 | 594 | 595 | 596 |
FilterSourceRendered
linky filter 576 |
<div ng-bind-html="snippet | linky">
</div>
577 |
579 |
580 |
linky target 585 |
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
586 |
588 |
589 |
no filter
<div ng-bind="snippet">
</div>
597 | 598 | 599 | it('should linkify the snippet with urls', function() { 600 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 601 | toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + 602 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 603 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); 604 | }); 605 | 606 | it('should not linkify snippet without the linky filter', function() { 607 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). 608 | toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + 609 | 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); 610 | expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); 611 | }); 612 | 613 | it('should update', function() { 614 | element(by.model('snippet')).clear(); 615 | element(by.model('snippet')).sendKeys('new http://link.'); 616 | expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 617 | toBe('new http://link.'); 618 | expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); 619 | expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) 620 | .toBe('new http://link.'); 621 | }); 622 | 623 | it('should work with the target property', function() { 624 | expect(element(by.id('linky-target')). 625 | element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). 626 | toBe('http://angularjs.org/'); 627 | expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); 628 | }); 629 | 630 | 631 | */ 632 | angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { 633 | var LINKY_URL_REGEXP = 634 | /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/i, 635 | MAILTO_REGEXP = /^mailto:/i; 636 | 637 | return function(text, target) { 638 | if (!text) return text; 639 | var match; 640 | var raw = text; 641 | var html = []; 642 | var url; 643 | var i; 644 | while ((match = raw.match(LINKY_URL_REGEXP))) { 645 | // We can not end in these as they are sometimes found at the end of the sentence 646 | url = match[0]; 647 | // if we did not match ftp/http/www/mailto then assume mailto 648 | if (!match[2] && !match[4]) { 649 | url = (match[3] ? 'http://' : 'mailto:') + url; 650 | } 651 | i = match.index; 652 | addText(raw.substr(0, i)); 653 | addLink(url, match[0].replace(MAILTO_REGEXP, '')); 654 | raw = raw.substring(i + match[0].length); 655 | } 656 | addText(raw); 657 | return $sanitize(html.join('')); 658 | 659 | function addText(text) { 660 | if (!text) { 661 | return; 662 | } 663 | html.push(sanitizeText(text)); 664 | } 665 | 666 | function addLink(url, text) { 667 | html.push(''); 676 | addText(text); 677 | html.push(''); 678 | } 679 | }; 680 | }]); 681 | 682 | 683 | })(window, window.angular); 684 | -------------------------------------------------------------------------------- /classes/PageScript.php: -------------------------------------------------------------------------------- 1 | template = $template; 19 | } 20 | 21 | public static function fromTemplate($template) 22 | { 23 | return new static($template); 24 | } 25 | 26 | /** 27 | * Populate the template object with the script attribute 28 | * @return void 29 | */ 30 | public function populate() 31 | { 32 | if (strlen($this->template->script)) 33 | return; 34 | 35 | $this->template->script = $this->readTemplateScript(); 36 | } 37 | 38 | /** 39 | * Save script content against a template, if the content does 40 | * not differ from the default, nothing is saved. 41 | * @param string $content 42 | * @return void 43 | */ 44 | public function save($content) 45 | { 46 | if ($content == $this->getDefaultScriptContent()) 47 | return; 48 | 49 | $this->writeTemplateScript($content); 50 | } 51 | 52 | /** 53 | * Returns the public path to the template script file. 54 | * @return string 55 | */ 56 | public function getPublicPath() 57 | { 58 | $path = $this->getTemplateScriptPath(); 59 | if (!File::isFile($path)) 60 | return; 61 | 62 | return File::localToPublic($path); 63 | } 64 | 65 | protected function getTemplateScriptPath() 66 | { 67 | $theme = Theme::getEditTheme(); 68 | $assetPath = $theme->getPath() . '/assets'; 69 | $fileName = $this->template->getBaseFileName(); 70 | 71 | $jsPath = $assetPath.'/javascript'; 72 | if (!File::isDirectory($jsPath)) $jsPath = $assetPath.'/js'; 73 | 74 | return $jsPath.'/controllers/'.$fileName.'.js'; 75 | } 76 | 77 | protected function readTemplateScript() 78 | { 79 | $path = $this->getTemplateScriptPath(); 80 | if (!File::isFile($path)) 81 | return $this->getDefaultScriptContent(); 82 | 83 | return $this->removeControllerDefinition(File::get($path)); 84 | } 85 | 86 | protected function writeTemplateScript($content) 87 | { 88 | $path = $this->getTemplateScriptPath(); 89 | $dir = dirname($path); 90 | if (!File::isDirectory($dir)) 91 | File::makeDirectory($dir, 0755, true); 92 | 93 | File::put($path, $this->addControllerDefinition($content)); 94 | } 95 | 96 | protected function getDefaultScriptContent() 97 | { 98 | return 'function ($scope, $request) {'.PHP_EOL.'}'; 99 | } 100 | 101 | protected function removeControllerDefinition($content) 102 | { 103 | return preg_replace('#^([\s]*'.static::JS_OBJECT.'.controllers\[[^\]]+\][\s]*=[\s]*)#si', '', $content); 104 | } 105 | 106 | protected function addControllerDefinition($content) 107 | { 108 | return sprintf("%s.controllers['%s'] = ", static::JS_OBJECT, $this->template->getBaseFileName()) . $content; 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /classes/ScopeBag.php: -------------------------------------------------------------------------------- 1 | toArray(); 28 | 29 | $this->vars[$key] = $value; 30 | return $this; 31 | } 32 | 33 | /** 34 | * Removes a value to the bag. 35 | * @param string $key 36 | * @param string $value 37 | * @return $this 38 | */ 39 | public function remove($key) 40 | { 41 | unset($this->vars[$key]); 42 | return $this; 43 | } 44 | 45 | /** 46 | * Merge a new array of vars into the bag. 47 | * @param array $vars 48 | * @return $this 49 | */ 50 | public function merge($vars) 51 | { 52 | $this->vars = array_merge_recursive($this->vars, $vars); 53 | return $this; 54 | } 55 | 56 | /** 57 | * Determine if value exist for a given key. 58 | * @param string $key 59 | * @return bool 60 | */ 61 | public function has($key = null) 62 | { 63 | return $this->first($key) !== ''; 64 | } 65 | 66 | /** 67 | * Get the first value from the bag for a given key. 68 | * @param string $key 69 | * @return string 70 | */ 71 | public function first($key = null) 72 | { 73 | $vars = is_null($key) ? $this->all() : $this->get($key); 74 | 75 | return (count($vars) > 0) ? $vars[0] : ''; 76 | } 77 | 78 | /** 79 | * Get all of the values from the bag for a given key. 80 | * @param string $key 81 | * @return array 82 | */ 83 | public function get($key) 84 | { 85 | if (array_key_exists($key, $this->vars)) 86 | return $this->vars[$key]; 87 | 88 | return null; 89 | } 90 | 91 | /** 92 | * Get all of the values for every key in the bag. 93 | * @param string $format 94 | * @return array 95 | */ 96 | public function all() 97 | { 98 | return $this->vars; 99 | } 100 | 101 | /** 102 | * Determine if the value bag has any vars. 103 | * 104 | * @return bool 105 | */ 106 | public function isEmpty() 107 | { 108 | return ! $this->any(); 109 | } 110 | 111 | /** 112 | * Determine if the value bag has any vars. 113 | * 114 | * @return bool 115 | */ 116 | public function any() 117 | { 118 | return $this->count() > 0; 119 | } 120 | 121 | /** 122 | * Get the number of vars in the container. 123 | * 124 | * @return int 125 | */ 126 | public function count() 127 | { 128 | return count($this->vars); 129 | } 130 | 131 | /** 132 | * Determine if the given attribute exists. 133 | * @param mixed $offset 134 | * @return bool 135 | */ 136 | public function offsetExists($offset) 137 | { 138 | return $this->has($offset); 139 | } 140 | 141 | /** 142 | * Get the value for a given offset. 143 | * @param mixed $offset 144 | * @return mixed 145 | */ 146 | public function offsetGet($offset) 147 | { 148 | return $this->get($offset); 149 | } 150 | 151 | /** 152 | * Set the value for a given offset. 153 | * @param mixed $offset 154 | * @param mixed $value 155 | * @return void 156 | */ 157 | public function offsetSet($offset, $value) 158 | { 159 | $this->add($offset, $value); 160 | } 161 | 162 | /** 163 | * Unset the value for a given offset. 164 | * @param mixed $offset 165 | * @return void 166 | */ 167 | public function offsetUnset($offset) 168 | { 169 | $this->remove($offset); 170 | } 171 | 172 | /** 173 | * Get the instance as an array. 174 | * 175 | * @return array 176 | */ 177 | public function toArray() 178 | { 179 | return $this->vars; 180 | } 181 | 182 | /** 183 | * Convert the object into something JSON serializable. 184 | * 185 | * @return array 186 | */ 187 | public function jsonSerialize() 188 | { 189 | return $this->toArray(); 190 | } 191 | 192 | /** 193 | * Convert the object to its JSON representation. 194 | * 195 | * @param int $options 196 | * @return string 197 | */ 198 | public function toJson($options = 0) 199 | { 200 | return json_encode($this->toArray(), $options); 201 | } 202 | 203 | /** 204 | * Convert the value bag to its string representation. 205 | * 206 | * @return string 207 | */ 208 | public function __toString() 209 | { 210 | return $this->toJson(); 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /components/Layout.php: -------------------------------------------------------------------------------- 1 | 'Angular Layout', 44 | 'description' => 'Allocate a layout for use with AngularJS.', 45 | ]; 46 | } 47 | 48 | public function defineProperties() 49 | { 50 | return [ 51 | 'idParam' => [ 52 | 'title' => 'Slug param name', 53 | 'description' => 'The URL route parameter used for looking up the channel by its slug. A hard coded slug can also be used.', 54 | 'default' => ':slug', 55 | 'type' => 'string', 56 | ], 57 | 58 | ]; 59 | } 60 | 61 | public function init() 62 | { 63 | $this->scope = $this->page['scope'] = new ScopeBag; 64 | // $this->addJs('assets/js/angular-bridge.js'); 65 | } 66 | 67 | public function onRun() 68 | { 69 | $this->pages = $this->page['pages'] = Page::all(); 70 | $this->dependencyString = $this->page['dependencyString'] = $this->getModuleDependencies(); 71 | } 72 | 73 | public function onGetPageDependencies() 74 | { 75 | $response = []; 76 | $this->pageCycle(); 77 | 78 | /* 79 | * Add the front-end controller, if available. 80 | */ 81 | $page = array_get($this->page['this'], 'page'); 82 | $pageScript = PageScript::fromTemplate($page); 83 | 84 | if ($scriptPath = $pageScript->getPublicPath()) 85 | $this->addJs($scriptPath.'?v='.$page->mtime); 86 | 87 | /* 88 | * Detect assets 89 | */ 90 | if ($this->controller->hasAssetsDefined()) { 91 | $response['X_OCTOBER_ASSETS'] = $this->controller->getAssetPaths(); 92 | } 93 | 94 | $response['scope'] = $this->scope; 95 | 96 | return $response; 97 | } 98 | 99 | public function addModuleDependency($name) 100 | { 101 | if (in_array($name, $this->dependencies)) 102 | return false; 103 | 104 | $this->dependencies[] = $name; 105 | return true; 106 | } 107 | 108 | public function getModuleDependencies() 109 | { 110 | return "['".implode("', '", $this->dependencies)."']"; 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /components/layout/default.htm: -------------------------------------------------------------------------------- 1 | 8 | 62 | -------------------------------------------------------------------------------- /updates/version.yaml: -------------------------------------------------------------------------------- 1 | 1.0.1: First version of Angular --------------------------------------------------------------------------------