aerogel-admin 1.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +13 -0
  6. data/Rakefile +1 -0
  7. data/aerogel-admin.gemspec +31 -0
  8. data/app/helpers/admin.rb +54 -0
  9. data/app/helpers/decorators.rb +29 -0
  10. data/app/helpers/icons.rb +11 -0
  11. data/app/helpers/table_builder.rb +8 -0
  12. data/app/helpers/tabs_builder.rb +7 -0
  13. data/app/routes/admin.rb +37 -0
  14. data/app/routes/users.rb +71 -0
  15. data/app/routes/users_access.rb +54 -0
  16. data/app/routes/users_roles.rb +53 -0
  17. data/assets/fonts/aerogel-icons.css +28 -0
  18. data/assets/fonts/aerogel-icons.eot +0 -0
  19. data/assets/fonts/aerogel-icons.svg +11 -0
  20. data/assets/fonts/aerogel-icons.ttf +0 -0
  21. data/assets/fonts/aerogel-icons.woff +0 -0
  22. data/assets/javascripts/.gitkeep +0 -0
  23. data/assets/javascripts/controllers/admin-modal.js.coffee +7 -0
  24. data/assets/javascripts/controllers/admin-modal/admin-modal-form-buttons.js.coffee +56 -0
  25. data/assets/javascripts/controllers/admin.js.coffee +13 -0
  26. data/assets/javascripts/controllers/admin/selectize-inputs.js.coffee +7 -0
  27. data/assets/javascripts/controllers/admin/top-menu-shadow.js.coffee +9 -0
  28. data/assets/javascripts/utils/ajax-spinner.js.coffee +26 -0
  29. data/assets/javascripts/utils/ajax-watcher.js.coffee +18 -0
  30. data/assets/javascripts/utils/bootstrap-modal-reload.js.coffee +8 -0
  31. data/assets/javascripts/utils/form-data-async.js.coffee +27 -0
  32. data/assets/javascripts/utils/i18n.js.coffee +27 -0
  33. data/assets/javascripts/utils/on-future-elements.js.coffee +15 -0
  34. data/assets/stylesheets/admin/bootstrap-settings.css.scss +33 -0
  35. data/assets/stylesheets/admin/global.css.scss +61 -0
  36. data/assets/stylesheets/admin/styles/ajax-indicator.css.scss +26 -0
  37. data/assets/stylesheets/admin/styles/bootstrap-modal.css.scss +24 -0
  38. data/assets/stylesheets/admin/styles/language-selector.css.scss +5 -0
  39. data/assets/stylesheets/admin/styles/page-header.css.scss +7 -0
  40. data/assets/stylesheets/admin/styles/sticky-footer-navbar.css.scss +34 -0
  41. data/assets/stylesheets/admin/styles/table.css.scss +3 -0
  42. data/assets/stylesheets/admin/styles/top-menu.css.scss +3 -0
  43. data/assets/stylesheets/admin/utils/center-absolutely.css.scss +5 -0
  44. data/assets/stylesheets/controllers/admin.css.scss +14 -0
  45. data/assets/vendor/bootstrap-datetimepicker.css.scss +1 -0
  46. data/assets/vendor/bootstrap-datetimepicker.js.coffee +1 -0
  47. data/assets/vendor/bootstrap-datetimepicker/bootstrap-datetimepicker.min.css +5 -0
  48. data/assets/vendor/bootstrap-datetimepicker/bootstrap-datetimepicker.min.js +1 -0
  49. data/assets/vendor/bootstrap-datetimepicker/bootstrap-datetimepicker.ru.js +163 -0
  50. data/assets/vendor/moment.js.coffee +1 -0
  51. data/assets/vendor/momentjs/moment-with-langs.min.js +9 -0
  52. data/assets/vendor/selectize.css.scss +2 -0
  53. data/assets/vendor/selectize.js.coffee +1 -0
  54. data/assets/vendor/selectize/selectize.bootstrap3.css +385 -0
  55. data/assets/vendor/selectize/selectize.css +311 -0
  56. data/assets/vendor/selectize/selectize.default.css +381 -0
  57. data/assets/vendor/selectize/selectize.js +3345 -0
  58. data/assets/vendor/smart-list-table.css.scss +42 -0
  59. data/assets/vendor/smart-list-table.js.coffee +1 -0
  60. data/assets/vendor/smart-list-table/smart-list-table-row.js.coffee +63 -0
  61. data/assets/vendor/smart-list-table/smart-list-table.css.scss +54 -0
  62. data/assets/vendor/smart-list-table/smart-list-table.js.coffee +133 -0
  63. data/assets/vendor/smart-tree-table.css.scss +42 -0
  64. data/assets/vendor/smart-tree-table.js.coffee +1 -0
  65. data/assets/vendor/smart-tree-table/smart-tree-table-drag-n-drop.js.coffee +190 -0
  66. data/assets/vendor/smart-tree-table/smart-tree-table-row.js.coffee +78 -0
  67. data/assets/vendor/smart-tree-table/smart-tree-table.css.scss +54 -0
  68. data/assets/vendor/smart-tree-table/smart-tree-table.js.coffee +267 -0
  69. data/assets/vendor/spin.js +353 -0
  70. data/config/README.md +3 -0
  71. data/config/development/.keep +0 -0
  72. data/config/production/.keep +0 -0
  73. data/db/model/README.md +1 -0
  74. data/db/model/admin/user_new_form.rb +40 -0
  75. data/db/model/user.rb +26 -0
  76. data/db/seed/01_admin_roles.seed +8 -0
  77. data/db/seed/02_admin_access.seed +24 -0
  78. data/db/seed/development/.keep +0 -0
  79. data/db/seed/development/20_users.seed +45 -0
  80. data/db/seed/development/admin_users.seed +38 -0
  81. data/db/seed/production/.keep +0 -0
  82. data/lib/aerogel/admin.rb +25 -0
  83. data/lib/aerogel/admin/core.rb +14 -0
  84. data/lib/aerogel/admin/menu.rb +38 -0
  85. data/lib/aerogel/admin/table_builder.rb +100 -0
  86. data/lib/aerogel/admin/tabs_builder.rb +69 -0
  87. data/lib/aerogel/admin/version.rb +5 -0
  88. data/locales/actions.en.yml +27 -0
  89. data/locales/actions.ru.yml +28 -0
  90. data/locales/admin.en.yml +14 -0
  91. data/locales/admin.ru.yml +14 -0
  92. data/locales/models.en.yml +8 -0
  93. data/locales/models.ru.yml +8 -0
  94. data/locales/views.en.yml +46 -0
  95. data/locales/views.ru.yml +46 -0
  96. data/public/README.md +1 -0
  97. data/rake/README.md +3 -0
  98. data/views/admin/index.html.erb +3 -0
  99. data/views/admin/table_builder/standard/_table_column.html.erb +3 -0
  100. data/views/admin/table_builder/standard/_table_row.html.erb +7 -0
  101. data/views/admin/table_builder/standard/table.html.erb +10 -0
  102. data/views/admin/tabs_builder/standard/_tab.html.erb +3 -0
  103. data/views/admin/tabs_builder/standard/tabs.html.erb +3 -0
  104. data/views/admin/users/_tabs.html.erb +8 -0
  105. data/views/admin/users/access/delete.html.erb +12 -0
  106. data/views/admin/users/access/edit.html.erb +9 -0
  107. data/views/admin/users/access/index.html.erb +23 -0
  108. data/views/admin/users/access/new.html.erb +9 -0
  109. data/views/admin/users/delete.html.erb +12 -0
  110. data/views/admin/users/edit.html.erb +46 -0
  111. data/views/admin/users/index.html.erb +31 -0
  112. data/views/admin/users/new.html.erb +11 -0
  113. data/views/admin/users/roles/delete.html.erb +12 -0
  114. data/views/admin/users/roles/edit.html.erb +9 -0
  115. data/views/admin/users/roles/index.html.erb +21 -0
  116. data/views/admin/users/roles/new.html.erb +7 -0
  117. data/views/form_builder/standard/field_multiselect.erb +14 -0
  118. data/views/form_builder/standard/field_select.erb +17 -0
  119. data/views/layouts/admin.html.erb +63 -0
  120. data/views/layouts/admin/_ajax_indicator.html.erb +4 -0
  121. data/views/layouts/admin/_alerts.html.erb +22 -0
  122. data/views/layouts/admin/_menu.html.erb +50 -0
  123. data/views/layouts/admin/_menu_item.html.erb +5 -0
  124. data/views/layouts/admin/_modals.html.erb +8 -0
  125. data/views/layouts/admin/modal.html.erb +38 -0
  126. metadata +280 -0
@@ -0,0 +1,78 @@
1
+
2
+ Function::property = (name) ->
3
+ Object.defineProperty @prototype, name,
4
+ get: -> @get_property name
5
+ set: (v) -> @set_property name, v
6
+
7
+ # An object representing tree table row.
8
+ #
9
+ #
10
+ class @SmartTreeTableRow
11
+
12
+ constructor: (@table, @el) ->
13
+ @id = @el.attr 'data-id'
14
+ @parent_id = @el.attr 'data-parent-id'
15
+ @parent_id = null if @parent_id == ''
16
+ @contents = @active_column().html()
17
+ @attributes = {}
18
+ for attr in @el.get(0).attributes
19
+ @attributes[attr.name] = attr.value
20
+ @_properties = {}
21
+
22
+
23
+ active_column: ->
24
+ @el.find(@table.settings.active_column_selector).first()
25
+
26
+ render: ->
27
+ @el.removeClass "#{@table.settings.selectedClass} branch leaf expanded collapsed"
28
+ @el.addClass @table.settings.selectedClass if @get_property 'selected'
29
+ @el.addClass if @get_property('branch') then 'branch' else 'leaf'
30
+ @el.addClass if @get_property('expanded') then 'expanded' else 'collapsed'
31
+ @active_column().html( "#{@_prefix()}#{@contents}")
32
+
33
+ show: ->
34
+ @el.show()
35
+
36
+ hide: ->
37
+ @el.hide()
38
+
39
+ # Returns attribute specified by +name+.
40
+ # 'data-...' attributes can be referenced by name without 'data-' prefix.
41
+ # Thus, 'data-id' can be accessed as:
42
+ # row.attr 'id'
43
+ #
44
+ attr: (name) ->
45
+ return @attributes[name] if @attributes[name]?
46
+ @attributes["data-#{name}"]
47
+
48
+ get_property: (name) ->
49
+ @_properties[name]
50
+ set_property: (name, v) ->
51
+ @_properties[name] = v
52
+ @render()
53
+ v
54
+
55
+ @property 'level'
56
+ @property 'expanded'
57
+ @property 'branch'
58
+ @property 'selected'
59
+
60
+
61
+ _indenter: (level) ->
62
+ px = level * @table.settings.indent
63
+ "<span class='indenter' style='padding-left:#{px}px'></span>"
64
+
65
+ _prefix: ->
66
+ p_indent = @_indenter @get_property 'level'
67
+ p_prefix = ''
68
+ s = {}
69
+ if @get_property 'branch'
70
+ s = @table.settings.prefix.branch
71
+ else
72
+ s = @table.settings.prefix.leaf
73
+ if @get_property 'expanded'
74
+ p_prefix = s.expanded
75
+ else
76
+ p_prefix = s.collapsed
77
+ "#{p_indent}#{p_prefix}"
78
+
@@ -0,0 +1,54 @@
1
+
2
+ .smart-tree-table tbody {
3
+
4
+ tr:hover {
5
+ background: $stt-hover-bg;
6
+ color: $stt-hover-fg;
7
+ cursor: $stt-hover-cursor;
8
+ }
9
+ tr.selected {
10
+ background: $stt-selected-bg;
11
+ color: $stt-selected-fg;
12
+ }
13
+
14
+ tr td span.indenter {
15
+ display: inline-block;
16
+ // margin: 0;
17
+ }
18
+
19
+ tr.drag-helper {
20
+ background: $stt-drag-helper-bg;
21
+ box-shadow: $stt-drag-helper-shadow;
22
+ border: $stt-drag-helper-border;
23
+ z-index: $stt-drag-helper-zindex;
24
+ }
25
+
26
+ tr.drag-over {
27
+ background: $stt-drag-over-bg;
28
+ color: $stt-drag-over-fg;
29
+ }
30
+
31
+ tr.drag-source {
32
+ background: $stt-drag-source-bg;
33
+ color: $stt-drag-source-fg;
34
+ }
35
+
36
+ .drop-target-overlay {
37
+ // border: 1px solid green;
38
+ }
39
+ .drop-target-overlay.before {
40
+ border-style: $stt-drop-target-overlay-before-border-style;
41
+ border-width: $stt-drop-target-overlay-before-border-width;
42
+ border-color: $stt-drop-target-overlay-before-border-color;
43
+ }
44
+ .drop-target-overlay.into {
45
+ border-style: $stt-drop-target-overlay-into-border-style;
46
+ border-width: $stt-drop-target-overlay-into-border-width;
47
+ border-color: $stt-drop-target-overlay-into-border-color;
48
+ }
49
+ .drop-target-overlay.after {
50
+ border-style: $stt-drop-target-overlay-after-border-style;
51
+ border-width: $stt-drop-target-overlay-after-border-width;
52
+ border-color: $stt-drop-target-overlay-after-border-color;
53
+ }
54
+ }
@@ -0,0 +1,267 @@
1
+ #= require ./smart-tree-table-row
2
+ #= require ./smart-tree-table-drag-n-drop
3
+ #
4
+ debug = false
5
+ log = (msg) ->
6
+ console?.log "** smart-tree-table: #{msg}" if debug
7
+ error = (msg) ->
8
+ console?.error "** smart-tree-table: #{msg}" if debug
9
+ throw new Error msg
10
+
11
+ class @SmartTreeTable
12
+
13
+
14
+
15
+ # Default settings
16
+ @_default_settings:
17
+ column: 1
18
+ indent: 20
19
+ addClass: 'smart-tree-table'
20
+ selectedClass: "selected"
21
+ hoverClass: null # or class to add on mouseenter/mouseleave
22
+ prefix:
23
+ branch:
24
+ expanded: "- "
25
+ collapsed: "+ "
26
+ leaf:
27
+ expanded: ""
28
+ collapsed: ""
29
+ dragAndDrop:
30
+ enabled: false
31
+ handle: null # or selector to drag-n-drop handle
32
+ helper: null # or function returning helper element
33
+ helperClass: 'drag-helper'
34
+ helperOpacity: 0.85
35
+ dragOverClass: 'drag-over'
36
+ dragSourceClass: 'drag-source'
37
+ dropTarget:
38
+ beforePx: 7
39
+ afterPx: 10
40
+ overlay:
41
+ class: "drop-target-overlay"
42
+ before: '' # contents for 'before' insert mode
43
+ into: '' # contents for 'into' insert mode
44
+ after: '' # contents for 'after' insert mode
45
+ debug: false
46
+ on_select: null
47
+ on_tree_change: null
48
+
49
+ # Creates a SmartTreeTable object using an HTML table specified by +el+ selector.
50
+ #
51
+ constructor: (el, options = {}) ->
52
+ unless $(el).length > 0
53
+ error "No matching elements specified by selector '#{el}'"
54
+ @settings = $.extend true, SmartTreeTable._default_settings, options
55
+ @settings.active_column_selector = "td:nth-child(#{@settings.column})"
56
+ @table = $(el).first()
57
+ @table.addClass @settings.addClass if @settings.addClass?
58
+ @init_table_rows()
59
+ @bind_event_listeners()
60
+ @selected_row = null
61
+ @drag_and_drop = null
62
+ $(el).disableSelection()
63
+ # SmartTreeTable object ready
64
+ #
65
+ @enable_drag_and_drop() if @settings.dragAndDrop.enabled
66
+ log "created SmartTreeTable: #{el}"
67
+
68
+
69
+ #
70
+ # private methods
71
+ #
72
+
73
+ # Returns table rows as list.
74
+ #
75
+ table_html_rows: ->
76
+ @table.find('tbody tr')
77
+
78
+ # Returns table row ids as list.
79
+ #
80
+ table_html_row_ids: ->
81
+ @id_of(row) for row in @table_html_rows()
82
+
83
+ # Returns Rows as list, ordered as the table.
84
+ #
85
+ rows_list: ->
86
+ @rows[id] for id in @table_html_row_ids()
87
+
88
+ # Returns list of children of parent with +id+.
89
+ #
90
+ children_of: (_id) ->
91
+ # log "children_of(#{_id}) #{typeof @rows}"
92
+ row for row in @rows_list() when row.parent_id == _id
93
+ # row for id, row of @rows when row.parent_id == _id
94
+
95
+ # Returns +id+ of a given row HTML element.
96
+ #
97
+ id_of: (el) ->
98
+ $(el).attr 'data-id'
99
+
100
+ # initializes table rows
101
+ #
102
+ init_table_rows: ->
103
+ @rows = {}
104
+ for row in @table_html_rows()
105
+ row_obj = new SmartTreeTableRow @, $(row)
106
+ row_obj.expanded = true
107
+ @rows[row_obj.id] = row_obj
108
+
109
+ @update_table_rows()
110
+ for row in @rows_list()
111
+ @collapse row.id
112
+ # log "row: #{row.id} -> #{row.parent_id} (level:#{row.level}): #{row.contents}"
113
+
114
+
115
+ # Updates table rows.
116
+ # Sets level, branch/leaf properties.
117
+ #
118
+ update_table_rows: ->
119
+ for row in @rows_list()
120
+ if row.parent_id?
121
+ row.level = @rows[row.parent_id].level + 1
122
+ else
123
+ row.level = 0
124
+ children = @children_of( row.id )
125
+ if children.length > 0
126
+ row.branch = true
127
+ else
128
+ row.branch = false
129
+ # log "row: #{row.id}, branch:#{row.branch}, children: #{children.length}"
130
+
131
+
132
+ # Collapses row with given +id+.
133
+ #
134
+ collapse: (id) ->
135
+ for row in @children_of(id)
136
+ @collapse row.id
137
+ row.hide()
138
+ @rows[id].expanded = false
139
+
140
+ # Expands row with given +id+.
141
+ #
142
+ expand: (id) ->
143
+ for row in @children_of(id)
144
+ row.show()
145
+ @rows[id].expanded = true
146
+
147
+ # Toggles collapsed/expanded row with +id+
148
+ #
149
+ toggle: (id) ->
150
+ if @rows[id].expanded
151
+ @collapse id
152
+ else
153
+ @expand id
154
+
155
+ # Selects row with +id+.
156
+ #
157
+ select: (id, process_callbacks = true ) ->
158
+ @selected_row.selected = false if @selected_row?
159
+ @selected_row = null
160
+ @selected_row = @rows[id] if id?
161
+ @selected_row.selected = true if @selected_row?
162
+ # if parent is collapsed, expand all until first expanded ancestor
163
+ if @selected_row? && @selected_row.parent_id?
164
+ parent_id = @selected_row.parent_id
165
+ while parent_id? && not @rows[parent_id].expanded
166
+ @expand parent_id
167
+ parent_id = @rows[parent_id].parent_id
168
+
169
+ if @settings.on_select? && process_callbacks
170
+ selected_id = if @selected_row? then @selected_row.id else null
171
+ selected_el = if @selected_row? then @selected_row.el else null
172
+ @settings.on_select selected_id, @selected_row, selected_el
173
+
174
+
175
+ # Binds event listeners on rows.
176
+ #
177
+ bind_event_listeners: ->
178
+ @table.find('tbody').on 'click', 'tr', (e) =>
179
+ id = @id_of $(e.currentTarget)
180
+ @select id
181
+
182
+ @table.find('tbody').on 'click', "tr #{@settings.active_column_selector}", (e) =>
183
+ id = @id_of $(e.currentTarget).closest('tr')
184
+ @toggle id
185
+
186
+ # this line of code below miraculously prevents event from
187
+ # not being propagated further, to "on 'click', 'tr'"
188
+ # (Chrome 33.0, jQuery 2.0.3)
189
+ tmp = "#{e.isPropagationStopped()}"
190
+ # log "column click: e stopped:#{e.isPropagationStopped()}"
191
+ # console.log e
192
+
193
+ if @settings.hoverClass?
194
+ @table.find('tbody').on 'mouseenter', 'tr', (e) =>
195
+ $(e.currentTarget).addClass @settings.hoverClass
196
+ @table.find('tbody').on 'mouseleave', 'tr', (e) =>
197
+ $(e.currentTarget).removeClass @settings.hoverClass
198
+
199
+ # Enables drag-n-drop for the table.
200
+ #
201
+ enable_drag_and_drop: =>
202
+ @drag_and_drop = new SmartTreeTableDragAndDrop @
203
+
204
+ # Disables drag-n-drop.
205
+ #
206
+ disable_drag_and_drop: ->
207
+ @table_html_rows().draggable 'destroy'
208
+ log "drag-n-drop disabled"
209
+
210
+
211
+ # Returns last element of a branch starting at +id+.
212
+ #
213
+ get_branch_last_element: (id) ->
214
+ children = @children_of id
215
+ return id unless children? && children.length > 0
216
+ @get_branch_last_element children[children.length-1].id # last of children
217
+
218
+ # Moves tree branch/leaf to another node
219
+ #
220
+ move_tree_node: (from_id, to_id, insert_mode) ->
221
+ source = @rows[from_id]
222
+ target = @rows[to_id]
223
+
224
+ # move around HTML elements
225
+ if insert_mode == 'before'
226
+ target.el.before( source.el )
227
+ else # after -- moves node after last element of target branch
228
+ new_target_id = @get_branch_last_element to_id
229
+ log "move_tree_node: after, to_id=#{to_id} new_target_id=#{new_target_id}"
230
+ target = @rows[new_target_id]
231
+ target.el.after( source.el )
232
+ @move_tree_node_children from_id
233
+
234
+ # update data structures
235
+ if insert_mode == 'into'
236
+ new_parent_id = to_id
237
+ else # before & after
238
+ new_parent_id = @rows[to_id].parent_id
239
+ console?.log "move_tree_node: new parent: #{new_parent_id}"
240
+ if new_parent_id?
241
+ source.parent_id = new_parent_id
242
+ source.el.attr 'data-parent-id', new_parent_id
243
+ else
244
+ source.parent_id = null
245
+ source.el.removeAttr 'data-parent-id'
246
+ @update_table_rows()
247
+
248
+ # finishing touches
249
+ @expand to_id if insert_mode == 'into'
250
+ if @settings.on_tree_change?
251
+ @settings.on_tree_change()
252
+
253
+
254
+
255
+ # Moves tree node children in place.
256
+ #
257
+ move_tree_node_children: (parent_id, last_element = null) ->
258
+ parent = @rows[parent_id]
259
+ last_element = parent.el unless last_element?
260
+ for row in @children_of parent_id
261
+ last_element.after row.el
262
+ last_element = row.el
263
+ last_element = @move_tree_node_children row.id, last_element
264
+ return last_element
265
+
266
+ $ ->
267
+ log "initialized"
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Copyright (c) 2011-2013 Felix Gnass
3
+ * Licensed under the MIT license
4
+ */
5
+ (function(root, factory) {
6
+
7
+ /* CommonJS */
8
+ if (typeof exports == 'object') module.exports = factory()
9
+
10
+ /* AMD module */
11
+ else if (typeof define == 'function' && define.amd) define(factory)
12
+
13
+ /* Browser global */
14
+ else root.Spinner = factory()
15
+ }
16
+ (this, function() {
17
+ "use strict";
18
+
19
+ var prefixes = ['webkit', 'Moz', 'ms', 'O'] /* Vendor prefixes */
20
+ , animations = {} /* Animation rules keyed by their name */
21
+ , useCssAnimations /* Whether to use CSS animations or setTimeout */
22
+
23
+ /**
24
+ * Utility function to create elements. If no tag name is given,
25
+ * a DIV is created. Optionally properties can be passed.
26
+ */
27
+ function createEl(tag, prop) {
28
+ var el = document.createElement(tag || 'div')
29
+ , n
30
+
31
+ for(n in prop) el[n] = prop[n]
32
+ return el
33
+ }
34
+
35
+ /**
36
+ * Appends children and returns the parent.
37
+ */
38
+ function ins(parent /* child1, child2, ...*/) {
39
+ for (var i=1, n=arguments.length; i<n; i++)
40
+ parent.appendChild(arguments[i])
41
+
42
+ return parent
43
+ }
44
+
45
+ /**
46
+ * Insert a new stylesheet to hold the @keyframe or VML rules.
47
+ */
48
+ var sheet = (function() {
49
+ var el = createEl('style', {type : 'text/css'})
50
+ ins(document.getElementsByTagName('head')[0], el)
51
+ return el.sheet || el.styleSheet
52
+ }())
53
+
54
+ /**
55
+ * Creates an opacity keyframe animation rule and returns its name.
56
+ * Since most mobile Webkits have timing issues with animation-delay,
57
+ * we create separate rules for each line/segment.
58
+ */
59
+ function addAnimation(alpha, trail, i, lines) {
60
+ var name = ['opacity', trail, ~~(alpha*100), i, lines].join('-')
61
+ , start = 0.01 + i/lines * 100
62
+ , z = Math.max(1 - (1-alpha) / trail * (100-start), alpha)
63
+ , prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase()
64
+ , pre = prefix && '-' + prefix + '-' || ''
65
+
66
+ if (!animations[name]) {
67
+ sheet.insertRule(
68
+ '@' + pre + 'keyframes ' + name + '{' +
69
+ '0%{opacity:' + z + '}' +
70
+ start + '%{opacity:' + alpha + '}' +
71
+ (start+0.01) + '%{opacity:1}' +
72
+ (start+trail) % 100 + '%{opacity:' + alpha + '}' +
73
+ '100%{opacity:' + z + '}' +
74
+ '}', sheet.cssRules.length)
75
+
76
+ animations[name] = 1
77
+ }
78
+
79
+ return name
80
+ }
81
+
82
+ /**
83
+ * Tries various vendor prefixes and returns the first supported property.
84
+ */
85
+ function vendor(el, prop) {
86
+ var s = el.style
87
+ , pp
88
+ , i
89
+
90
+ prop = prop.charAt(0).toUpperCase() + prop.slice(1)
91
+ for(i=0; i<prefixes.length; i++) {
92
+ pp = prefixes[i]+prop
93
+ if(s[pp] !== undefined) return pp
94
+ }
95
+ if(s[prop] !== undefined) return prop
96
+ }
97
+
98
+ /**
99
+ * Sets multiple style properties at once.
100
+ */
101
+ function css(el, prop) {
102
+ for (var n in prop)
103
+ el.style[vendor(el, n)||n] = prop[n]
104
+
105
+ return el
106
+ }
107
+
108
+ /**
109
+ * Fills in default values.
110
+ */
111
+ function merge(obj) {
112
+ for (var i=1; i < arguments.length; i++) {
113
+ var def = arguments[i]
114
+ for (var n in def)
115
+ if (obj[n] === undefined) obj[n] = def[n]
116
+ }
117
+ return obj
118
+ }
119
+
120
+ /**
121
+ * Returns the absolute page-offset of the given element.
122
+ */
123
+ function pos(el) {
124
+ var o = { x:el.offsetLeft, y:el.offsetTop }
125
+ while((el = el.offsetParent))
126
+ o.x+=el.offsetLeft, o.y+=el.offsetTop
127
+
128
+ return o
129
+ }
130
+
131
+ /**
132
+ * Returns the line color from the given string or array.
133
+ */
134
+ function getColor(color, idx) {
135
+ return typeof color == 'string' ? color : color[idx % color.length]
136
+ }
137
+
138
+ // Built-in defaults
139
+
140
+ var defaults = {
141
+ lines: 12, // The number of lines to draw
142
+ length: 7, // The length of each line
143
+ width: 5, // The line thickness
144
+ radius: 10, // The radius of the inner circle
145
+ rotate: 0, // Rotation offset
146
+ corners: 1, // Roundness (0..1)
147
+ color: '#000', // #rgb or #rrggbb
148
+ direction: 1, // 1: clockwise, -1: counterclockwise
149
+ speed: 1, // Rounds per second
150
+ trail: 100, // Afterglow percentage
151
+ opacity: 1/4, // Opacity of the lines
152
+ fps: 20, // Frames per second when using setTimeout()
153
+ zIndex: 2e9, // Use a high z-index by default
154
+ className: 'spinner', // CSS class to assign to the element
155
+ top: 'auto', // center vertically
156
+ left: 'auto', // center horizontally
157
+ position: 'relative' // element position
158
+ }
159
+
160
+ /** The constructor */
161
+ function Spinner(o) {
162
+ if (typeof this == 'undefined') return new Spinner(o)
163
+ this.opts = merge(o || {}, Spinner.defaults, defaults)
164
+ }
165
+
166
+ // Global defaults that override the built-ins:
167
+ Spinner.defaults = {}
168
+
169
+ merge(Spinner.prototype, {
170
+
171
+ /**
172
+ * Adds the spinner to the given target element. If this instance is already
173
+ * spinning, it is automatically removed from its previous target b calling
174
+ * stop() internally.
175
+ */
176
+ spin: function(target) {
177
+ this.stop()
178
+
179
+ var self = this
180
+ , o = self.opts
181
+ , el = self.el = css(createEl(0, {className: o.className}), {position: o.position, width: 0, zIndex: o.zIndex})
182
+ , mid = o.radius+o.length+o.width
183
+ , ep // element position
184
+ , tp // target position
185
+
186
+ if (target) {
187
+ target.insertBefore(el, target.firstChild||null)
188
+ tp = pos(target)
189
+ ep = pos(el)
190
+ css(el, {
191
+ left: (o.left == 'auto' ? tp.x-ep.x + (target.offsetWidth >> 1) : parseInt(o.left, 10) + mid) + 'px',
192
+ top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + 'px'
193
+ })
194
+ }
195
+
196
+ el.setAttribute('role', 'progressbar')
197
+ self.lines(el, self.opts)
198
+
199
+ if (!useCssAnimations) {
200
+ // No CSS animation support, use setTimeout() instead
201
+ var i = 0
202
+ , start = (o.lines - 1) * (1 - o.direction) / 2
203
+ , alpha
204
+ , fps = o.fps
205
+ , f = fps/o.speed
206
+ , ostep = (1-o.opacity) / (f*o.trail / 100)
207
+ , astep = f/o.lines
208
+
209
+ ;(function anim() {
210
+ i++;
211
+ for (var j = 0; j < o.lines; j++) {
212
+ alpha = Math.max(1 - (i + (o.lines - j) * astep) % f * ostep, o.opacity)
213
+
214
+ self.opacity(el, j * o.direction + start, alpha, o)
215
+ }
216
+ self.timeout = self.el && setTimeout(anim, ~~(1000/fps))
217
+ })()
218
+ }
219
+ return self
220
+ },
221
+
222
+ /**
223
+ * Stops and removes the Spinner.
224
+ */
225
+ stop: function() {
226
+ var el = this.el
227
+ if (el) {
228
+ clearTimeout(this.timeout)
229
+ if (el.parentNode) el.parentNode.removeChild(el)
230
+ this.el = undefined
231
+ }
232
+ return this
233
+ },
234
+
235
+ /**
236
+ * Internal method that draws the individual lines. Will be overwritten
237
+ * in VML fallback mode below.
238
+ */
239
+ lines: function(el, o) {
240
+ var i = 0
241
+ , start = (o.lines - 1) * (1 - o.direction) / 2
242
+ , seg
243
+
244
+ function fill(color, shadow) {
245
+ return css(createEl(), {
246
+ position: 'absolute',
247
+ width: (o.length+o.width) + 'px',
248
+ height: o.width + 'px',
249
+ background: color,
250
+ boxShadow: shadow,
251
+ transformOrigin: 'left',
252
+ transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)',
253
+ borderRadius: (o.corners * o.width>>1) + 'px'
254
+ })
255
+ }
256
+
257
+ for (; i < o.lines; i++) {
258
+ seg = css(createEl(), {
259
+ position: 'absolute',
260
+ top: 1+~(o.width/2) + 'px',
261
+ transform: o.hwaccel ? 'translate3d(0,0,0)' : '',
262
+ opacity: o.opacity,
263
+ animation: useCssAnimations && addAnimation(o.opacity, o.trail, start + i * o.direction, o.lines) + ' ' + 1/o.speed + 's linear infinite'
264
+ })
265
+
266
+ if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'}))
267
+ ins(el, ins(seg, fill(getColor(o.color, i), '0 0 1px rgba(0,0,0,.1)')))
268
+ }
269
+ return el
270
+ },
271
+
272
+ /**
273
+ * Internal method that adjusts the opacity of a single line.
274
+ * Will be overwritten in VML fallback mode below.
275
+ */
276
+ opacity: function(el, i, val) {
277
+ if (i < el.childNodes.length) el.childNodes[i].style.opacity = val
278
+ }
279
+
280
+ })
281
+
282
+
283
+ function initVML() {
284
+
285
+ /* Utility function to create a VML tag */
286
+ function vml(tag, attr) {
287
+ return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr)
288
+ }
289
+
290
+ // No CSS transforms but VML support, add a CSS rule for VML elements:
291
+ sheet.addRule('.spin-vml', 'behavior:url(#default#VML)')
292
+
293
+ Spinner.prototype.lines = function(el, o) {
294
+ var r = o.length+o.width
295
+ , s = 2*r
296
+
297
+ function grp() {
298
+ return css(
299
+ vml('group', {
300
+ coordsize: s + ' ' + s,
301
+ coordorigin: -r + ' ' + -r
302
+ }),
303
+ { width: s, height: s }
304
+ )
305
+ }
306
+
307
+ var margin = -(o.width+o.length)*2 + 'px'
308
+ , g = css(grp(), {position: 'absolute', top: margin, left: margin})
309
+ , i
310
+
311
+ function seg(i, dx, filter) {
312
+ ins(g,
313
+ ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}),
314
+ ins(css(vml('roundrect', {arcsize: o.corners}), {
315
+ width: r,
316
+ height: o.width,
317
+ left: o.radius,
318
+ top: -o.width>>1,
319
+ filter: filter
320
+ }),
321
+ vml('fill', {color: getColor(o.color, i), opacity: o.opacity}),
322
+ vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change
323
+ )
324
+ )
325
+ )
326
+ }
327
+
328
+ if (o.shadow)
329
+ for (i = 1; i <= o.lines; i++)
330
+ seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)')
331
+
332
+ for (i = 1; i <= o.lines; i++) seg(i)
333
+ return ins(el, g)
334
+ }
335
+
336
+ Spinner.prototype.opacity = function(el, i, val, o) {
337
+ var c = el.firstChild
338
+ o = o.shadow && o.lines || 0
339
+ if (c && i+o < c.childNodes.length) {
340
+ c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild
341
+ if (c) c.opacity = val
342
+ }
343
+ }
344
+ }
345
+
346
+ var probe = css(createEl('group'), {behavior: 'url(#default#VML)'})
347
+
348
+ if (!vendor(probe, 'transform') && probe.adj) initVML()
349
+ else useCssAnimations = vendor(probe, 'animation')
350
+
351
+ return Spinner
352
+
353
+ }));