aerogel-admin 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }));