voltar 0.0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +37 -0
  5. data/app/assets/fonts/FontAwesome.otf +0 -0
  6. data/app/assets/fonts/Simple-Line-Icons.eot +0 -0
  7. data/app/assets/fonts/Simple-Line-Icons.svg +1369 -0
  8. data/app/assets/fonts/Simple-Line-Icons.ttf +0 -0
  9. data/app/assets/fonts/Simple-Line-Icons.woff +0 -0
  10. data/app/assets/fonts/fontawesome-webfont.eot +0 -0
  11. data/app/assets/fonts/fontawesome-webfont.svg +520 -0
  12. data/app/assets/fonts/fontawesome-webfont.ttf +0 -0
  13. data/app/assets/fonts/fontawesome-webfont.woff +0 -0
  14. data/app/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  15. data/app/assets/fonts/glyphicons-halflings-regular.svg +229 -0
  16. data/app/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  17. data/app/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  18. data/app/assets/fonts/sourcesanspro/sourcesanspro-bold.woff +0 -0
  19. data/app/assets/fonts/sourcesanspro/sourcesanspro-light.woff +0 -0
  20. data/app/assets/fonts/sourcesanspro/sourcesanspro.woff +0 -0
  21. data/app/assets/javascripts/voltar/app.js +203 -0
  22. data/app/assets/javascripts/voltar/application.js +33 -0
  23. data/app/assets/javascripts/voltar/controllers/app_ctrl.js.erb +673 -0
  24. data/app/assets/javascripts/voltar/directives/app_directives.js +345 -0
  25. data/app/assets/javascripts/voltar/factories/app_services.js +255 -0
  26. data/app/assets/javascripts/voltar/stripe.js.coffee +2 -0
  27. data/app/assets/stylesheets/voltar/app.css +4990 -0
  28. data/app/assets/stylesheets/voltar/application.css +23 -0
  29. data/app/assets/stylesheets/voltar/voltar.css.scss +93 -0
  30. data/app/controllers/voltar/application_controller.rb +4 -0
  31. data/app/controllers/voltar/dashboard_controller.rb +11 -0
  32. data/app/helpers/voltar/application_helper.rb +4 -0
  33. data/app/views/layouts/voltar/application.html.erb +40 -0
  34. data/app/views/voltar/account/_billing.html.erb +552 -0
  35. data/app/views/voltar/account/_locations.html.erb +135 -0
  36. data/app/views/voltar/account/_managers.html +134 -0
  37. data/app/views/voltar/account/_password.html.erb +57 -0
  38. data/app/views/voltar/account/_profile.html.erb +84 -0
  39. data/app/views/voltar/dashboard/index.html.erb +0 -0
  40. data/app/views/voltar/inventory/_delete_dialog.html.erb +16 -0
  41. data/app/views/voltar/inventory/_edit.html.erb +244 -0
  42. data/app/views/voltar/inventory/_index.html.erb +160 -0
  43. data/app/views/voltar/inventory/_mark_as_sold_dialog.html.erb +26 -0
  44. data/app/views/voltar/shared/_keen_js.html.haml +11 -0
  45. data/app/views/voltar/shared/_voltar_app.html.erb +83 -0
  46. data/app/views/voltar/shared/_voltar_aside.html.erb +71 -0
  47. data/app/views/voltar/shared/_voltar_footer.html.erb +13 -0
  48. data/app/views/voltar/shared/_voltar_header.html.erb +156 -0
  49. data/app/views/voltar/shared/app/_country_province_select.html +19 -0
  50. data/app/views/voltar/shared/app/_dashboard.html.erb +243 -0
  51. data/app/views/voltar/shared/app/_notifications.html.erb +13 -0
  52. data/app/views/voltar/shared/app/_spinner.html.erb +8 -0
  53. data/config/routes.rb +4 -0
  54. data/lib/tasks/voltar_tasks.rake +4 -0
  55. data/lib/voltar.rb +5 -0
  56. data/lib/voltar/engine.rb +15 -0
  57. data/lib/voltar/version.rb +3 -0
  58. data/test/dummy/README.rdoc +28 -0
  59. data/test/dummy/Rakefile +6 -0
  60. data/test/dummy/app/assets/javascripts/application.js +13 -0
  61. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  62. data/test/dummy/app/controllers/application_controller.rb +5 -0
  63. data/test/dummy/app/helpers/application_helper.rb +2 -0
  64. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  65. data/test/dummy/bin/bundle +3 -0
  66. data/test/dummy/bin/rails +4 -0
  67. data/test/dummy/bin/rake +4 -0
  68. data/test/dummy/bin/setup +29 -0
  69. data/test/dummy/config.ru +4 -0
  70. data/test/dummy/config/application.rb +26 -0
  71. data/test/dummy/config/boot.rb +5 -0
  72. data/test/dummy/config/database.yml +25 -0
  73. data/test/dummy/config/environment.rb +5 -0
  74. data/test/dummy/config/environments/development.rb +41 -0
  75. data/test/dummy/config/environments/production.rb +76 -0
  76. data/test/dummy/config/environments/test.rb +39 -0
  77. data/test/dummy/config/initializers/assets.rb +11 -0
  78. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  79. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  80. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  81. data/test/dummy/config/initializers/inflections.rb +16 -0
  82. data/test/dummy/config/initializers/mime_types.rb +4 -0
  83. data/test/dummy/config/initializers/session_store.rb +3 -0
  84. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  85. data/test/dummy/config/locales/en.yml +23 -0
  86. data/test/dummy/config/routes.rb +4 -0
  87. data/test/dummy/config/secrets.yml +22 -0
  88. data/test/dummy/log/development.log +0 -0
  89. data/test/dummy/public/404.html +67 -0
  90. data/test/dummy/public/422.html +67 -0
  91. data/test/dummy/public/500.html +66 -0
  92. data/test/dummy/public/favicon.ico +0 -0
  93. data/test/integration/navigation_test.rb +10 -0
  94. data/test/test_helper.rb +15 -0
  95. data/test/voltar_test.rb +7 -0
  96. data/vendor/assets/images/voltar/a0.jpg +0 -0
  97. data/vendor/assets/images/voltar/a1.jpg +0 -0
  98. data/vendor/assets/images/voltar/a10.jpg +0 -0
  99. data/vendor/assets/images/voltar/a2.jpg +0 -0
  100. data/vendor/assets/images/voltar/a3.jpg +0 -0
  101. data/vendor/assets/images/voltar/a4.jpg +0 -0
  102. data/vendor/assets/images/voltar/a5.jpg +0 -0
  103. data/vendor/assets/images/voltar/a6.jpg +0 -0
  104. data/vendor/assets/images/voltar/a7.jpg +0 -0
  105. data/vendor/assets/images/voltar/a8.jpg +0 -0
  106. data/vendor/assets/images/voltar/a9.jpg +0 -0
  107. data/vendor/assets/images/voltar/b0.jpg +0 -0
  108. data/vendor/assets/images/voltar/b1.jpg +0 -0
  109. data/vendor/assets/images/voltar/b2.jpg +0 -0
  110. data/vendor/assets/images/voltar/b3.jpg +0 -0
  111. data/vendor/assets/images/voltar/b4.jpg +0 -0
  112. data/vendor/assets/images/voltar/b5.jpg +0 -0
  113. data/vendor/assets/images/voltar/c0.jpg +0 -0
  114. data/vendor/assets/images/voltar/c1.jpg +0 -0
  115. data/vendor/assets/images/voltar/c2.jpg +0 -0
  116. data/vendor/assets/images/voltar/c3.jpg +0 -0
  117. data/vendor/assets/images/voltar/c4.jpg +0 -0
  118. data/vendor/assets/images/voltar/c5.jpg +0 -0
  119. data/vendor/assets/images/voltar/chosen-sprite.png +0 -0
  120. data/vendor/assets/images/voltar/chosen-sprite@2x.png +0 -0
  121. data/vendor/assets/images/voltar/logo.png +0 -0
  122. data/vendor/assets/images/voltar/p0.jpg +0 -0
  123. data/vendor/assets/javascripts/voltar/angular-animate.js +1689 -0
  124. data/vendor/assets/javascripts/voltar/angular-contenteditable.js +98 -0
  125. data/vendor/assets/javascripts/voltar/angular-cookies.js +206 -0
  126. data/vendor/assets/javascripts/voltar/angular-sanitize.js +647 -0
  127. data/vendor/assets/javascripts/voltar/angular-ui-router.js +3658 -0
  128. data/vendor/assets/javascripts/voltar/angular.js +22024 -0
  129. data/vendor/assets/javascripts/voltar/chosen.jquery.min.js +2 -0
  130. data/vendor/assets/javascripts/voltar/easypiechart/jquery.easy-pie-chart.js +209 -0
  131. data/vendor/assets/javascripts/voltar/flot/jquery.flot.min.js +29 -0
  132. data/vendor/assets/javascripts/voltar/flot/jquery.flot.orderBars.js +187 -0
  133. data/vendor/assets/javascripts/voltar/flot/jquery.flot.pie.min.js +56 -0
  134. data/vendor/assets/javascripts/voltar/flot/jquery.flot.resize.js +60 -0
  135. data/vendor/assets/javascripts/voltar/flot/jquery.flot.spline.js +212 -0
  136. data/vendor/assets/javascripts/voltar/flot/jquery.flot.tooltip.min.js +12 -0
  137. data/vendor/assets/javascripts/voltar/jquery.min.js +5 -0
  138. data/vendor/assets/javascripts/voltar/moment.js +2856 -0
  139. data/vendor/assets/javascripts/voltar/ngStorage.js +103 -0
  140. data/vendor/assets/javascripts/voltar/ocLazyLoad.js +906 -0
  141. data/vendor/assets/javascripts/voltar/smart-table.min.js +1 -0
  142. data/vendor/assets/javascripts/voltar/sparkline/jquery.sparkline.min.js +2 -0
  143. data/vendor/assets/javascripts/voltar/toaster.js +185 -0
  144. data/vendor/assets/javascripts/voltar/ui-bootstrap-tpls.js +4116 -0
  145. data/vendor/assets/javascripts/voltar/ui-jq.js +86 -0
  146. data/vendor/assets/javascripts/voltar/ui-load.js +93 -0
  147. data/vendor/assets/javascripts/voltar/ui-validate.js +119 -0
  148. data/vendor/assets/stylesheets/voltar/animate.css +1098 -0
  149. data/vendor/assets/stylesheets/voltar/bootstrap.css +6202 -0
  150. data/vendor/assets/stylesheets/voltar/chosen.css +399 -0
  151. data/vendor/assets/stylesheets/voltar/font-awesome.min.css +4 -0
  152. data/vendor/assets/stylesheets/voltar/font.css +18 -0
  153. data/vendor/assets/stylesheets/voltar/simple-line-icons.css +526 -0
  154. data/vendor/assets/stylesheets/voltar/toaster.css +213 -0
  155. metadata +333 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @see http://docs.angularjs.org/guide/concepts
3
+ * @see http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController
4
+ * @see https://github.com/angular/angular.js/issues/528#issuecomment-7573166
5
+ */
6
+
7
+ angular.module('contenteditable', [])
8
+ .directive('contenteditable', ['$timeout', function($timeout) { return {
9
+ restrict: 'A',
10
+ require: '?ngModel',
11
+ link: function(scope, element, attrs, ngModel) {
12
+ // don't do anything unless this is actually bound to a model
13
+ if (!ngModel) {
14
+ return
15
+ }
16
+
17
+ // options
18
+ var opts = {}
19
+ angular.forEach([
20
+ 'stripBr',
21
+ 'noLineBreaks',
22
+ 'selectNonEditable',
23
+ 'moveCaretToEndOnChange',
24
+ ], function(opt) {
25
+ var o = attrs[opt]
26
+ opts[opt] = o && o !== 'false'
27
+ })
28
+
29
+ // view -> model
30
+ element.bind('input', function(e) {
31
+ scope.$apply(function() {
32
+ var html, html2, rerender
33
+ html = element.html()
34
+ rerender = false
35
+ if (opts.stripBr) {
36
+ html = html.replace(/<br>$/, '')
37
+ }
38
+ if (opts.noLineBreaks) {
39
+ html2 = html.replace(/<div>/g, '').replace(/<br>/g, '').replace(/<\/div>/g, '')
40
+ if (html2 !== html) {
41
+ rerender = true
42
+ html = html2
43
+ }
44
+ }
45
+ ngModel.$setViewValue(html)
46
+ if (rerender) {
47
+ ngModel.$render()
48
+ }
49
+ if (html === '') {
50
+ // the cursor disappears if the contents is empty
51
+ // so we need to refocus
52
+ $timeout(function(){
53
+ element[0].blur()
54
+ element[0].focus()
55
+ })
56
+ }
57
+ })
58
+ })
59
+
60
+ // model -> view
61
+ var oldRender = ngModel.$render
62
+ ngModel.$render = function() {
63
+ var el, el2, range, sel
64
+ if (!!oldRender) {
65
+ oldRender()
66
+ }
67
+ element.html(ngModel.$viewValue || '')
68
+ if (opts.moveCaretToEndOnChange) {
69
+ el = element[0]
70
+ range = document.createRange()
71
+ sel = window.getSelection()
72
+ if (el.childNodes.length > 0) {
73
+ el2 = el.childNodes[el.childNodes.length - 1]
74
+ range.setStartAfter(el2)
75
+ } else {
76
+ range.setStartAfter(el)
77
+ }
78
+ range.collapse(true)
79
+ sel.removeAllRanges()
80
+ sel.addRange(range)
81
+ }
82
+ }
83
+ if (opts.selectNonEditable) {
84
+ element.bind('click', function(e) {
85
+ var range, sel, target
86
+ target = e.toElement
87
+ if (target !== this && angular.element(target).attr('contenteditable') === 'false') {
88
+ range = document.createRange()
89
+ sel = window.getSelection()
90
+ range.setStartBefore(target)
91
+ range.setEndAfter(target)
92
+ sel.removeAllRanges()
93
+ sel.addRange(range)
94
+ }
95
+ })
96
+ }
97
+ }
98
+ }}])
@@ -0,0 +1,206 @@
1
+ /**
2
+ * @license AngularJS v1.2.25
3
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {'use strict';
7
+
8
+ /**
9
+ * @ngdoc module
10
+ * @name ngCookies
11
+ * @description
12
+ *
13
+ * # ngCookies
14
+ *
15
+ * The `ngCookies` module provides a convenient wrapper for reading and writing browser cookies.
16
+ *
17
+ *
18
+ * <div doc-module-components="ngCookies"></div>
19
+ *
20
+ * See {@link ngCookies.$cookies `$cookies`} and
21
+ * {@link ngCookies.$cookieStore `$cookieStore`} for usage.
22
+ */
23
+
24
+
25
+ angular.module('ngCookies', ['ng']).
26
+ /**
27
+ * @ngdoc service
28
+ * @name $cookies
29
+ *
30
+ * @description
31
+ * Provides read/write access to browser's cookies.
32
+ *
33
+ * Only a simple Object is exposed and by adding or removing properties to/from this object, new
34
+ * cookies are created/deleted at the end of current $eval.
35
+ * The object's properties can only be strings.
36
+ *
37
+ * Requires the {@link ngCookies `ngCookies`} module to be installed.
38
+ *
39
+ * @example
40
+ *
41
+ * ```js
42
+ * angular.module('cookiesExample', ['ngCookies'])
43
+ * .controller('ExampleController', ['$cookies', function($cookies) {
44
+ * // Retrieving a cookie
45
+ * var favoriteCookie = $cookies.myFavorite;
46
+ * // Setting a cookie
47
+ * $cookies.myFavorite = 'oatmeal';
48
+ * }]);
49
+ * ```
50
+ */
51
+ factory('$cookies', ['$rootScope', '$browser', function ($rootScope, $browser) {
52
+ var cookies = {},
53
+ lastCookies = {},
54
+ lastBrowserCookies,
55
+ runEval = false,
56
+ copy = angular.copy,
57
+ isUndefined = angular.isUndefined;
58
+
59
+ //creates a poller fn that copies all cookies from the $browser to service & inits the service
60
+ $browser.addPollFn(function() {
61
+ var currentCookies = $browser.cookies();
62
+ if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl
63
+ lastBrowserCookies = currentCookies;
64
+ copy(currentCookies, lastCookies);
65
+ copy(currentCookies, cookies);
66
+ if (runEval) $rootScope.$apply();
67
+ }
68
+ })();
69
+
70
+ runEval = true;
71
+
72
+ //at the end of each eval, push cookies
73
+ //TODO: this should happen before the "delayed" watches fire, because if some cookies are not
74
+ // strings or browser refuses to store some cookies, we update the model in the push fn.
75
+ $rootScope.$watch(push);
76
+
77
+ return cookies;
78
+
79
+
80
+ /**
81
+ * Pushes all the cookies from the service to the browser and verifies if all cookies were
82
+ * stored.
83
+ */
84
+ function push() {
85
+ var name,
86
+ value,
87
+ browserCookies,
88
+ updated;
89
+
90
+ //delete any cookies deleted in $cookies
91
+ for (name in lastCookies) {
92
+ if (isUndefined(cookies[name])) {
93
+ $browser.cookies(name, undefined);
94
+ }
95
+ }
96
+
97
+ //update all cookies updated in $cookies
98
+ for(name in cookies) {
99
+ value = cookies[name];
100
+ if (!angular.isString(value)) {
101
+ value = '' + value;
102
+ cookies[name] = value;
103
+ }
104
+ if (value !== lastCookies[name]) {
105
+ $browser.cookies(name, value);
106
+ updated = true;
107
+ }
108
+ }
109
+
110
+ //verify what was actually stored
111
+ if (updated){
112
+ updated = false;
113
+ browserCookies = $browser.cookies();
114
+
115
+ for (name in cookies) {
116
+ if (cookies[name] !== browserCookies[name]) {
117
+ //delete or reset all cookies that the browser dropped from $cookies
118
+ if (isUndefined(browserCookies[name])) {
119
+ delete cookies[name];
120
+ } else {
121
+ cookies[name] = browserCookies[name];
122
+ }
123
+ updated = true;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }]).
129
+
130
+
131
+ /**
132
+ * @ngdoc service
133
+ * @name $cookieStore
134
+ * @requires $cookies
135
+ *
136
+ * @description
137
+ * Provides a key-value (string-object) storage, that is backed by session cookies.
138
+ * Objects put or retrieved from this storage are automatically serialized or
139
+ * deserialized by angular's toJson/fromJson.
140
+ *
141
+ * Requires the {@link ngCookies `ngCookies`} module to be installed.
142
+ *
143
+ * @example
144
+ *
145
+ * ```js
146
+ * angular.module('cookieStoreExample', ['ngCookies'])
147
+ * .controller('ExampleController', ['$cookieStore', function($cookieStore) {
148
+ * // Put cookie
149
+ * $cookieStore.put('myFavorite','oatmeal');
150
+ * // Get cookie
151
+ * var favoriteCookie = $cookieStore.get('myFavorite');
152
+ * // Removing a cookie
153
+ * $cookieStore.remove('myFavorite');
154
+ * }]);
155
+ * ```
156
+ */
157
+ factory('$cookieStore', ['$cookies', function($cookies) {
158
+
159
+ return {
160
+ /**
161
+ * @ngdoc method
162
+ * @name $cookieStore#get
163
+ *
164
+ * @description
165
+ * Returns the value of given cookie key
166
+ *
167
+ * @param {string} key Id to use for lookup.
168
+ * @returns {Object} Deserialized cookie value.
169
+ */
170
+ get: function(key) {
171
+ var value = $cookies[key];
172
+ return value ? angular.fromJson(value) : value;
173
+ },
174
+
175
+ /**
176
+ * @ngdoc method
177
+ * @name $cookieStore#put
178
+ *
179
+ * @description
180
+ * Sets a value for given cookie key
181
+ *
182
+ * @param {string} key Id for the `value`.
183
+ * @param {Object} value Value to be stored.
184
+ */
185
+ put: function(key, value) {
186
+ $cookies[key] = angular.toJson(value);
187
+ },
188
+
189
+ /**
190
+ * @ngdoc method
191
+ * @name $cookieStore#remove
192
+ *
193
+ * @description
194
+ * Remove given cookie
195
+ *
196
+ * @param {string} key Id of the key-value pair to delete.
197
+ */
198
+ remove: function(key) {
199
+ delete $cookies[key];
200
+ }
201
+ };
202
+
203
+ }]);
204
+
205
+
206
+ })(window, window.angular);
@@ -0,0 +1,647 @@
1
+ /**
2
+ * @license AngularJS v1.2.25
3
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {'use strict';
7
+
8
+ var $sanitizeMinErr = angular.$$minErr('$sanitize');
9
+
10
+ /**
11
+ * @ngdoc module
12
+ * @name ngSanitize
13
+ * @description
14
+ *
15
+ * # ngSanitize
16
+ *
17
+ * The `ngSanitize` module provides functionality to sanitize HTML.
18
+ *
19
+ *
20
+ * <div doc-module-components="ngSanitize"></div>
21
+ *
22
+ * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
23
+ */
24
+
25
+ /*
26
+ * HTML Parser By Misko Hevery (misko@hevery.com)
27
+ * based on: HTML Parser By John Resig (ejohn.org)
28
+ * Original code by Erik Arvidsson, Mozilla Public License
29
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
30
+ *
31
+ * // Use like so:
32
+ * htmlParser(htmlString, {
33
+ * start: function(tag, attrs, unary) {},
34
+ * end: function(tag) {},
35
+ * chars: function(text) {},
36
+ * comment: function(text) {}
37
+ * });
38
+ *
39
+ */
40
+
41
+
42
+ /**
43
+ * @ngdoc service
44
+ * @name $sanitize
45
+ * @kind function
46
+ *
47
+ * @description
48
+ * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
49
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
50
+ * it into the returned string, however, since our parser is more strict than a typical browser
51
+ * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
52
+ * browser, won't make it through the sanitizer.
53
+ * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
54
+ * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
55
+ *
56
+ * @param {string} html Html input.
57
+ * @returns {string} Sanitized html.
58
+ *
59
+ * @example
60
+ <example module="sanitizeExample" deps="angular-sanitize.js">
61
+ <file name="index.html">
62
+ <script>
63
+ angular.module('sanitizeExample', ['ngSanitize'])
64
+ .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
65
+ $scope.snippet =
66
+ '<p style="color:blue">an html\n' +
67
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
68
+ 'snippet</p>';
69
+ $scope.deliberatelyTrustDangerousSnippet = function() {
70
+ return $sce.trustAsHtml($scope.snippet);
71
+ };
72
+ }]);
73
+ </script>
74
+ <div ng-controller="ExampleController">
75
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
76
+ <table>
77
+ <tr>
78
+ <td>Directive</td>
79
+ <td>How</td>
80
+ <td>Source</td>
81
+ <td>Rendered</td>
82
+ </tr>
83
+ <tr id="bind-html-with-sanitize">
84
+ <td>ng-bind-html</td>
85
+ <td>Automatically uses $sanitize</td>
86
+ <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
87
+ <td><div ng-bind-html="snippet"></div></td>
88
+ </tr>
89
+ <tr id="bind-html-with-trust">
90
+ <td>ng-bind-html</td>
91
+ <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
92
+ <td>
93
+ <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
94
+ &lt;/div&gt;</pre>
95
+ </td>
96
+ <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
97
+ </tr>
98
+ <tr id="bind-default">
99
+ <td>ng-bind</td>
100
+ <td>Automatically escapes</td>
101
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
102
+ <td><div ng-bind="snippet"></div></td>
103
+ </tr>
104
+ </table>
105
+ </div>
106
+ </file>
107
+ <file name="protractor.js" type="protractor">
108
+ it('should sanitize the html snippet by default', function() {
109
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
110
+ toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
111
+ });
112
+
113
+ it('should inline raw snippet if bound to a trusted value', function() {
114
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
115
+ toBe("<p style=\"color:blue\">an html\n" +
116
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
117
+ "snippet</p>");
118
+ });
119
+
120
+ it('should escape snippet without any filter', function() {
121
+ expect(element(by.css('#bind-default div')).getInnerHtml()).
122
+ toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
123
+ "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
124
+ "snippet&lt;/p&gt;");
125
+ });
126
+
127
+ it('should update', function() {
128
+ element(by.model('snippet')).clear();
129
+ element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
130
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
131
+ toBe('new <b>text</b>');
132
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
133
+ 'new <b onclick="alert(1)">text</b>');
134
+ expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
135
+ "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
136
+ });
137
+ </file>
138
+ </example>
139
+ */
140
+ function $SanitizeProvider() {
141
+ this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
142
+ return function(html) {
143
+ var buf = [];
144
+ htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
145
+ return !/^unsafe/.test($$sanitizeUri(uri, isImage));
146
+ }));
147
+ return buf.join('');
148
+ };
149
+ }];
150
+ }
151
+
152
+ function sanitizeText(chars) {
153
+ var buf = [];
154
+ var writer = htmlSanitizeWriter(buf, angular.noop);
155
+ writer.chars(chars);
156
+ return buf.join('');
157
+ }
158
+
159
+
160
+ // Regular Expressions for parsing tags and attributes
161
+ var START_TAG_REGEXP =
162
+ /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
163
+ END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
164
+ ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
165
+ BEGIN_TAG_REGEXP = /^</,
166
+ BEGING_END_TAGE_REGEXP = /^<\//,
167
+ COMMENT_REGEXP = /<!--(.*?)-->/g,
168
+ DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
169
+ CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
170
+ SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
171
+ // Match everything outside of normal chars and " (quote character)
172
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
173
+
174
+
175
+ // Good source of info about elements and attributes
176
+ // http://dev.w3.org/html5/spec/Overview.html#semantics
177
+ // http://simon.html5.org/html-elements
178
+
179
+ // Safe Void Elements - HTML5
180
+ // http://dev.w3.org/html5/spec/Overview.html#void-elements
181
+ var voidElements = makeMap("area,br,col,hr,img,wbr");
182
+
183
+ // Elements that you can, intentionally, leave open (and which close themselves)
184
+ // http://dev.w3.org/html5/spec/Overview.html#optional-tags
185
+ var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
186
+ optionalEndTagInlineElements = makeMap("rp,rt"),
187
+ optionalEndTagElements = angular.extend({},
188
+ optionalEndTagInlineElements,
189
+ optionalEndTagBlockElements);
190
+
191
+ // Safe Block Elements - HTML5
192
+ var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
193
+ "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
194
+ "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
195
+
196
+ // Inline Elements - HTML5
197
+ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
198
+ "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
199
+ "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
200
+
201
+
202
+ // Special Elements (can contain anything)
203
+ var specialElements = makeMap("script,style");
204
+
205
+ var validElements = angular.extend({},
206
+ voidElements,
207
+ blockElements,
208
+ inlineElements,
209
+ optionalEndTagElements);
210
+
211
+ //Attributes that have href and hence need to be sanitized
212
+ var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
213
+ var validAttrs = angular.extend({}, uriAttrs, makeMap(
214
+ 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
215
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
216
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
217
+ 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
218
+ 'valign,value,vspace,width'));
219
+
220
+ function makeMap(str) {
221
+ var obj = {}, items = str.split(','), i;
222
+ for (i = 0; i < items.length; i++) obj[items[i]] = true;
223
+ return obj;
224
+ }
225
+
226
+
227
+ /**
228
+ * @example
229
+ * htmlParser(htmlString, {
230
+ * start: function(tag, attrs, unary) {},
231
+ * end: function(tag) {},
232
+ * chars: function(text) {},
233
+ * comment: function(text) {}
234
+ * });
235
+ *
236
+ * @param {string} html string
237
+ * @param {object} handler
238
+ */
239
+ function htmlParser( html, handler ) {
240
+ if (typeof html !== 'string') {
241
+ if (html === null || typeof html === 'undefined') {
242
+ html = '';
243
+ } else {
244
+ html = '' + html;
245
+ }
246
+ }
247
+ var index, chars, match, stack = [], last = html, text;
248
+ stack.last = function() { return stack[ stack.length - 1 ]; };
249
+
250
+ while ( html ) {
251
+ text = '';
252
+ chars = true;
253
+
254
+ // Make sure we're not in a script or style element
255
+ if ( !stack.last() || !specialElements[ stack.last() ] ) {
256
+
257
+ // Comment
258
+ if ( html.indexOf("<!--") === 0 ) {
259
+ // comments containing -- are not allowed unless they terminate the comment
260
+ index = html.indexOf("--", 4);
261
+
262
+ if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
263
+ if (handler.comment) handler.comment( html.substring( 4, index ) );
264
+ html = html.substring( index + 3 );
265
+ chars = false;
266
+ }
267
+ // DOCTYPE
268
+ } else if ( DOCTYPE_REGEXP.test(html) ) {
269
+ match = html.match( DOCTYPE_REGEXP );
270
+
271
+ if ( match ) {
272
+ html = html.replace( match[0], '');
273
+ chars = false;
274
+ }
275
+ // end tag
276
+ } else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
277
+ match = html.match( END_TAG_REGEXP );
278
+
279
+ if ( match ) {
280
+ html = html.substring( match[0].length );
281
+ match[0].replace( END_TAG_REGEXP, parseEndTag );
282
+ chars = false;
283
+ }
284
+
285
+ // start tag
286
+ } else if ( BEGIN_TAG_REGEXP.test(html) ) {
287
+ match = html.match( START_TAG_REGEXP );
288
+
289
+ if ( match ) {
290
+ // We only have a valid start-tag if there is a '>'.
291
+ if ( match[4] ) {
292
+ html = html.substring( match[0].length );
293
+ match[0].replace( START_TAG_REGEXP, parseStartTag );
294
+ }
295
+ chars = false;
296
+ } else {
297
+ // no ending tag found --- this piece should be encoded as an entity.
298
+ text += '<';
299
+ html = html.substring(1);
300
+ }
301
+ }
302
+
303
+ if ( chars ) {
304
+ index = html.indexOf("<");
305
+
306
+ text += index < 0 ? html : html.substring( 0, index );
307
+ html = index < 0 ? "" : html.substring( index );
308
+
309
+ if (handler.chars) handler.chars( decodeEntities(text) );
310
+ }
311
+
312
+ } else {
313
+ html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
314
+ function(all, text){
315
+ text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
316
+
317
+ if (handler.chars) handler.chars( decodeEntities(text) );
318
+
319
+ return "";
320
+ });
321
+
322
+ parseEndTag( "", stack.last() );
323
+ }
324
+
325
+ if ( html == last ) {
326
+ throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
327
+ "of html: {0}", html);
328
+ }
329
+ last = html;
330
+ }
331
+
332
+ // Clean up any remaining tags
333
+ parseEndTag();
334
+
335
+ function parseStartTag( tag, tagName, rest, unary ) {
336
+ tagName = angular.lowercase(tagName);
337
+ if ( blockElements[ tagName ] ) {
338
+ while ( stack.last() && inlineElements[ stack.last() ] ) {
339
+ parseEndTag( "", stack.last() );
340
+ }
341
+ }
342
+
343
+ if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
344
+ parseEndTag( "", tagName );
345
+ }
346
+
347
+ unary = voidElements[ tagName ] || !!unary;
348
+
349
+ if ( !unary )
350
+ stack.push( tagName );
351
+
352
+ var attrs = {};
353
+
354
+ rest.replace(ATTR_REGEXP,
355
+ function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
356
+ var value = doubleQuotedValue
357
+ || singleQuotedValue
358
+ || unquotedValue
359
+ || '';
360
+
361
+ attrs[name] = decodeEntities(value);
362
+ });
363
+ if (handler.start) handler.start( tagName, attrs, unary );
364
+ }
365
+
366
+ function parseEndTag( tag, tagName ) {
367
+ var pos = 0, i;
368
+ tagName = angular.lowercase(tagName);
369
+ if ( tagName )
370
+ // Find the closest opened tag of the same type
371
+ for ( pos = stack.length - 1; pos >= 0; pos-- )
372
+ if ( stack[ pos ] == tagName )
373
+ break;
374
+
375
+ if ( pos >= 0 ) {
376
+ // Close all the open elements, up the stack
377
+ for ( i = stack.length - 1; i >= pos; i-- )
378
+ if (handler.end) handler.end( stack[ i ] );
379
+
380
+ // Remove the open elements from the stack
381
+ stack.length = pos;
382
+ }
383
+ }
384
+ }
385
+
386
+ var hiddenPre=document.createElement("pre");
387
+ var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
388
+ /**
389
+ * decodes all entities into regular string
390
+ * @param value
391
+ * @returns {string} A string with decoded entities.
392
+ */
393
+ function decodeEntities(value) {
394
+ if (!value) { return ''; }
395
+
396
+ // Note: IE8 does not preserve spaces at the start/end of innerHTML
397
+ // so we must capture them and reattach them afterward
398
+ var parts = spaceRe.exec(value);
399
+ var spaceBefore = parts[1];
400
+ var spaceAfter = parts[3];
401
+ var content = parts[2];
402
+ if (content) {
403
+ hiddenPre.innerHTML=content.replace(/</g,"&lt;");
404
+ // innerText depends on styling as it doesn't display hidden elements.
405
+ // Therefore, it's better to use textContent not to cause unnecessary
406
+ // reflows. However, IE<9 don't support textContent so the innerText
407
+ // fallback is necessary.
408
+ content = 'textContent' in hiddenPre ?
409
+ hiddenPre.textContent : hiddenPre.innerText;
410
+ }
411
+ return spaceBefore + content + spaceAfter;
412
+ }
413
+
414
+ /**
415
+ * Escapes all potentially dangerous characters, so that the
416
+ * resulting string can be safely inserted into attribute or
417
+ * element text.
418
+ * @param value
419
+ * @returns {string} escaped text
420
+ */
421
+ function encodeEntities(value) {
422
+ return value.
423
+ replace(/&/g, '&amp;').
424
+ replace(SURROGATE_PAIR_REGEXP, function (value) {
425
+ var hi = value.charCodeAt(0);
426
+ var low = value.charCodeAt(1);
427
+ return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
428
+ }).
429
+ replace(NON_ALPHANUMERIC_REGEXP, function(value){
430
+ return '&#' + value.charCodeAt(0) + ';';
431
+ }).
432
+ replace(/</g, '&lt;').
433
+ replace(/>/g, '&gt;');
434
+ }
435
+
436
+ /**
437
+ * create an HTML/XML writer which writes to buffer
438
+ * @param {Array} buf use buf.jain('') to get out sanitized html string
439
+ * @returns {object} in the form of {
440
+ * start: function(tag, attrs, unary) {},
441
+ * end: function(tag) {},
442
+ * chars: function(text) {},
443
+ * comment: function(text) {}
444
+ * }
445
+ */
446
+ function htmlSanitizeWriter(buf, uriValidator){
447
+ var ignore = false;
448
+ var out = angular.bind(buf, buf.push);
449
+ return {
450
+ start: function(tag, attrs, unary){
451
+ tag = angular.lowercase(tag);
452
+ if (!ignore && specialElements[tag]) {
453
+ ignore = tag;
454
+ }
455
+ if (!ignore && validElements[tag] === true) {
456
+ out('<');
457
+ out(tag);
458
+ angular.forEach(attrs, function(value, key){
459
+ var lkey=angular.lowercase(key);
460
+ var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
461
+ if (validAttrs[lkey] === true &&
462
+ (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
463
+ out(' ');
464
+ out(key);
465
+ out('="');
466
+ out(encodeEntities(value));
467
+ out('"');
468
+ }
469
+ });
470
+ out(unary ? '/>' : '>');
471
+ }
472
+ },
473
+ end: function(tag){
474
+ tag = angular.lowercase(tag);
475
+ if (!ignore && validElements[tag] === true) {
476
+ out('</');
477
+ out(tag);
478
+ out('>');
479
+ }
480
+ if (tag == ignore) {
481
+ ignore = false;
482
+ }
483
+ },
484
+ chars: function(chars){
485
+ if (!ignore) {
486
+ out(encodeEntities(chars));
487
+ }
488
+ }
489
+ };
490
+ }
491
+
492
+
493
+ // define ngSanitize module and register $sanitize service
494
+ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
495
+
496
+ /* global sanitizeText: false */
497
+
498
+ /**
499
+ * @ngdoc filter
500
+ * @name linky
501
+ * @kind function
502
+ *
503
+ * @description
504
+ * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
505
+ * plain email address links.
506
+ *
507
+ * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
508
+ *
509
+ * @param {string} text Input text.
510
+ * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
511
+ * @returns {string} Html-linkified text.
512
+ *
513
+ * @usage
514
+ <span ng-bind-html="linky_expression | linky"></span>
515
+ *
516
+ * @example
517
+ <example module="linkyExample" deps="angular-sanitize.js">
518
+ <file name="index.html">
519
+ <script>
520
+ angular.module('linkyExample', ['ngSanitize'])
521
+ .controller('ExampleController', ['$scope', function($scope) {
522
+ $scope.snippet =
523
+ 'Pretty text with some links:\n'+
524
+ 'http://angularjs.org/,\n'+
525
+ 'mailto:us@somewhere.org,\n'+
526
+ 'another@somewhere.org,\n'+
527
+ 'and one more: ftp://127.0.0.1/.';
528
+ $scope.snippetWithTarget = 'http://angularjs.org/';
529
+ }]);
530
+ </script>
531
+ <div ng-controller="ExampleController">
532
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
533
+ <table>
534
+ <tr>
535
+ <td>Filter</td>
536
+ <td>Source</td>
537
+ <td>Rendered</td>
538
+ </tr>
539
+ <tr id="linky-filter">
540
+ <td>linky filter</td>
541
+ <td>
542
+ <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
543
+ </td>
544
+ <td>
545
+ <div ng-bind-html="snippet | linky"></div>
546
+ </td>
547
+ </tr>
548
+ <tr id="linky-target">
549
+ <td>linky target</td>
550
+ <td>
551
+ <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
552
+ </td>
553
+ <td>
554
+ <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
555
+ </td>
556
+ </tr>
557
+ <tr id="escaped-html">
558
+ <td>no filter</td>
559
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
560
+ <td><div ng-bind="snippet"></div></td>
561
+ </tr>
562
+ </table>
563
+ </file>
564
+ <file name="protractor.js" type="protractor">
565
+ it('should linkify the snippet with urls', function() {
566
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
567
+ toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
568
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
569
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
570
+ });
571
+
572
+ it('should not linkify snippet without the linky filter', function() {
573
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
574
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
575
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
576
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
577
+ });
578
+
579
+ it('should update', function() {
580
+ element(by.model('snippet')).clear();
581
+ element(by.model('snippet')).sendKeys('new http://link.');
582
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
583
+ toBe('new http://link.');
584
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
585
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
586
+ .toBe('new http://link.');
587
+ });
588
+
589
+ it('should work with the target property', function() {
590
+ expect(element(by.id('linky-target')).
591
+ element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
592
+ toBe('http://angularjs.org/');
593
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
594
+ });
595
+ </file>
596
+ </example>
597
+ */
598
+ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
599
+ var LINKY_URL_REGEXP =
600
+ /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
601
+ MAILTO_REGEXP = /^mailto:/;
602
+
603
+ return function(text, target) {
604
+ if (!text) return text;
605
+ var match;
606
+ var raw = text;
607
+ var html = [];
608
+ var url;
609
+ var i;
610
+ while ((match = raw.match(LINKY_URL_REGEXP))) {
611
+ // We can not end in these as they are sometimes found at the end of the sentence
612
+ url = match[0];
613
+ // if we did not match ftp/http/mailto then assume mailto
614
+ if (match[2] == match[3]) url = 'mailto:' + url;
615
+ i = match.index;
616
+ addText(raw.substr(0, i));
617
+ addLink(url, match[0].replace(MAILTO_REGEXP, ''));
618
+ raw = raw.substring(i + match[0].length);
619
+ }
620
+ addText(raw);
621
+ return $sanitize(html.join(''));
622
+
623
+ function addText(text) {
624
+ if (!text) {
625
+ return;
626
+ }
627
+ html.push(sanitizeText(text));
628
+ }
629
+
630
+ function addLink(url, text) {
631
+ html.push('<a ');
632
+ if (angular.isDefined(target)) {
633
+ html.push('target="');
634
+ html.push(target);
635
+ html.push('" ');
636
+ }
637
+ html.push('href="');
638
+ html.push(url);
639
+ html.push('">');
640
+ addText(text);
641
+ html.push('</a>');
642
+ }
643
+ };
644
+ }]);
645
+
646
+
647
+ })(window, window.angular);