asset_host_core 2.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (199) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.markdown +138 -0
  3. data/Rakefile +11 -0
  4. data/app/assets/images/asset_host_core/alert-overlay.png +0 -0
  5. data/app/assets/images/asset_host_core/arrow-left.gif +0 -0
  6. data/app/assets/images/asset_host_core/arrow-right.gif +0 -0
  7. data/app/assets/images/asset_host_core/fallback-img-rect.png +0 -0
  8. data/app/assets/images/asset_host_core/videoplayer-play.png +0 -0
  9. data/app/assets/images/asset_host_core/x.png +0 -0
  10. data/app/assets/javascripts/asset_host_core/admin/assets.js.coffee +221 -0
  11. data/app/assets/javascripts/asset_host_core/application.js +20 -0
  12. data/app/assets/javascripts/asset_host_core/assetadmin.js.coffee +56 -0
  13. data/app/assets/javascripts/asset_host_core/assethost.js.coffee.erb +17 -0
  14. data/app/assets/javascripts/asset_host_core/browserui.js.coffee +139 -0
  15. data/app/assets/javascripts/asset_host_core/chooserui.js.coffee +381 -0
  16. data/app/assets/javascripts/asset_host_core/client.js.coffee +29 -0
  17. data/app/assets/javascripts/asset_host_core/clients/BrightcoveVideo.js.coffee +64 -0
  18. data/app/assets/javascripts/asset_host_core/clients/templates/brightcove_embed.jst.eco +18 -0
  19. data/app/assets/javascripts/asset_host_core/clients/templates/vimeo_embed.jst.eco +1 -0
  20. data/app/assets/javascripts/asset_host_core/clients/templates/youtube_embed.jst.eco +1 -0
  21. data/app/assets/javascripts/asset_host_core/clients/vimeo_video.js.coffee +21 -0
  22. data/app/assets/javascripts/asset_host_core/clients/youtube_video.js.coffee +21 -0
  23. data/app/assets/javascripts/asset_host_core/cmsplugin.js.coffee +235 -0
  24. data/app/assets/javascripts/asset_host_core/models.js.coffee +586 -0
  25. data/app/assets/javascripts/asset_host_core/railsCMS.js.coffee +141 -0
  26. data/app/assets/javascripts/asset_host_core/slideshow.js.coffee +428 -0
  27. data/app/assets/javascripts/asset_host_core/templates/after_upload_button.jst.eco +3 -0
  28. data/app/assets/javascripts/asset_host_core/templates/asset_drop_asset.jst.eco +4 -0
  29. data/app/assets/javascripts/asset_host_core/templates/asset_modal.jst.eco +13 -0
  30. data/app/assets/javascripts/asset_host_core/templates/asset_preview.jst.eco +35 -0
  31. data/app/assets/javascripts/asset_host_core/templates/asset_search.jst.eco +2 -0
  32. data/app/assets/javascripts/asset_host_core/templates/browser_asset.jst.eco +1 -0
  33. data/app/assets/javascripts/asset_host_core/templates/browser_asset_tip.jst.eco +3 -0
  34. data/app/assets/javascripts/asset_host_core/templates/edit_modal.jst.eco +40 -0
  35. data/app/assets/javascripts/asset_host_core/templates/import_help.jst.eco +59 -0
  36. data/app/assets/javascripts/asset_host_core/templates/pagination_link.jst.eco +1 -0
  37. data/app/assets/javascripts/asset_host_core/templates/pagination_links.jst.eco +13 -0
  38. data/app/assets/javascripts/asset_host_core/templates/queued_file.jst.eco +11 -0
  39. data/app/assets/javascripts/asset_host_core/templates/save_and_close_view.jst.eco +4 -0
  40. data/app/assets/javascripts/asset_host_core/templates/upload_all_button.jst.eco +4 -0
  41. data/app/assets/javascripts/asset_host_core/templates/url_input.jst.eco +8 -0
  42. data/app/assets/stylesheets/asset_host_core/application.css.scss +384 -0
  43. data/app/assets/stylesheets/asset_host_core/jquery-ui.css +105 -0
  44. data/app/assets/stylesheets/asset_host_core/public.css.scss +204 -0
  45. data/app/assets/stylesheets/asset_host_core/slidetest.css.scss +93 -0
  46. data/app/controllers/asset_host_core/admin/api_users_controller.rb +72 -0
  47. data/app/controllers/asset_host_core/admin/assets_controller.rb +140 -0
  48. data/app/controllers/asset_host_core/admin/base_controller.rb +36 -0
  49. data/app/controllers/asset_host_core/admin/home_controller.rb +13 -0
  50. data/app/controllers/asset_host_core/admin/outputs_controller.rb +55 -0
  51. data/app/controllers/asset_host_core/api/assets_controller.rb +110 -0
  52. data/app/controllers/asset_host_core/api/base_controller.rb +43 -0
  53. data/app/controllers/asset_host_core/api/outputs_controller.rb +33 -0
  54. data/app/controllers/asset_host_core/application_controller.rb +43 -0
  55. data/app/controllers/asset_host_core/public_controller.rb +104 -0
  56. data/app/models/asset_host_core/api_user.rb +44 -0
  57. data/app/models/asset_host_core/api_user_permission.rb +6 -0
  58. data/app/models/asset_host_core/asset.rb +265 -0
  59. data/app/models/asset_host_core/asset_output.rb +69 -0
  60. data/app/models/asset_host_core/brightcove_video.rb +20 -0
  61. data/app/models/asset_host_core/output.rb +52 -0
  62. data/app/models/asset_host_core/permission.rb +19 -0
  63. data/app/models/asset_host_core/video.rb +8 -0
  64. data/app/models/asset_host_core/vimeo_video.rb +17 -0
  65. data/app/models/asset_host_core/youtube_video.rb +17 -0
  66. data/app/views/asset_host_core/admin/api_users/_form_fields.html.erb +5 -0
  67. data/app/views/asset_host_core/admin/api_users/edit.html.erb +26 -0
  68. data/app/views/asset_host_core/admin/api_users/index.html.erb +31 -0
  69. data/app/views/asset_host_core/admin/api_users/new.html.erb +17 -0
  70. data/app/views/asset_host_core/admin/api_users/show.html.erb +23 -0
  71. data/app/views/asset_host_core/admin/assets/index.html.erb +19 -0
  72. data/app/views/asset_host_core/admin/assets/metadata.html.erb +24 -0
  73. data/app/views/asset_host_core/admin/assets/show.html.erb +86 -0
  74. data/app/views/asset_host_core/admin/home/chooser.html.erb +49 -0
  75. data/app/views/asset_host_core/admin/outputs/_form_fields.html.erb +5 -0
  76. data/app/views/asset_host_core/admin/outputs/edit.html.erb +26 -0
  77. data/app/views/asset_host_core/admin/outputs/index.html.erb +27 -0
  78. data/app/views/asset_host_core/admin/outputs/new.html.erb +13 -0
  79. data/app/views/asset_host_core/admin/outputs/show.html.erb +17 -0
  80. data/app/views/asset_host_core/shared/_footerjs.html.erb +3 -0
  81. data/app/views/asset_host_core/shared/_navbar.html.erb +28 -0
  82. data/app/views/kaminari/_first_page.html.erb +3 -0
  83. data/app/views/kaminari/_gap.html.erb +3 -0
  84. data/app/views/kaminari/_last_page.html.erb +3 -0
  85. data/app/views/kaminari/_next_page.html.erb +3 -0
  86. data/app/views/kaminari/_page.html.erb +3 -0
  87. data/app/views/kaminari/_paginator.html.erb +17 -0
  88. data/app/views/kaminari/_prev_page.html.erb +3 -0
  89. data/app/views/layouts/asset_host_core/application.html.erb +54 -0
  90. data/app/views/layouts/asset_host_core/full_width.html.erb +32 -0
  91. data/app/views/layouts/asset_host_core/minimal.html.erb +45 -0
  92. data/config/initializers/simple_form.rb +142 -0
  93. data/config/initializers/simple_form_bootstrap.rb +45 -0
  94. data/config/locales/simple_form.en.yml +26 -0
  95. data/config/routes.rb +49 -0
  96. data/lib/asset_host_core.rb +38 -0
  97. data/lib/asset_host_core/config.rb +39 -0
  98. data/lib/asset_host_core/engine.rb +94 -0
  99. data/lib/asset_host_core/loaders.rb +34 -0
  100. data/lib/asset_host_core/loaders/asset_host.rb +30 -0
  101. data/lib/asset_host_core/loaders/base.rb +22 -0
  102. data/lib/asset_host_core/loaders/brightcove.rb +67 -0
  103. data/lib/asset_host_core/loaders/flickr.rb +114 -0
  104. data/lib/asset_host_core/loaders/url.rb +59 -0
  105. data/lib/asset_host_core/loaders/vimeo.rb +76 -0
  106. data/lib/asset_host_core/loaders/youtube.rb +90 -0
  107. data/lib/asset_host_core/model_methods.rb +61 -0
  108. data/lib/asset_host_core/paperclip.rb +4 -0
  109. data/lib/asset_host_core/paperclip/asset_thumbnail.rb +92 -0
  110. data/lib/asset_host_core/paperclip/attachment.rb +206 -0
  111. data/lib/asset_host_core/paperclip/trimmer.rb +33 -0
  112. data/lib/asset_host_core/resque_job.rb +13 -0
  113. data/lib/asset_host_core/version.rb +3 -0
  114. data/lib/tasks/asset_host_core_tasks.rake +4 -0
  115. data/spec/controllers/admin/api_users_controller_spec.rb +21 -0
  116. data/spec/controllers/admin/assets_controller_spec.rb +59 -0
  117. data/spec/controllers/admin/home_controller_spec.rb +4 -0
  118. data/spec/controllers/admin/outputs_controller_spec.rb +4 -0
  119. data/spec/controllers/api/assets_controller_spec.rb +133 -0
  120. data/spec/controllers/api/outputs_controller_spec.rb +51 -0
  121. data/spec/controllers/public_controller_spec.rb +4 -0
  122. data/spec/factories.rb +39 -0
  123. data/spec/features/api_users_spec.rb +78 -0
  124. data/spec/fixtures/api/brightcove/video.json +137 -0
  125. data/spec/fixtures/api/flickr/photos_getInfo.json +78 -0
  126. data/spec/fixtures/api/flickr/photos_getSizes.json +82 -0
  127. data/spec/fixtures/api/flickr/photos_licenses_getInfo.json +52 -0
  128. data/spec/fixtures/api/vimeo/video.json +28 -0
  129. data/spec/fixtures/api/youtube/discovery.json +5190 -0
  130. data/spec/fixtures/api/youtube/video.json +44 -0
  131. data/spec/fixtures/images/chipmunk.jpg +0 -0
  132. data/spec/fixtures/images/dude.jpg +0 -0
  133. data/spec/fixtures/images/ernie.jpg +0 -0
  134. data/spec/fixtures/images/fry.png +0 -0
  135. data/spec/fixtures/images/hat.jpg +0 -0
  136. data/spec/fixtures/images/spongebob.png +0 -0
  137. data/spec/fixtures/images/stars.jpg +0 -0
  138. data/spec/internal/app/controllers/application_controller.rb +16 -0
  139. data/spec/internal/app/controllers/sessions_controller.rb +24 -0
  140. data/spec/internal/app/models/user.rb +10 -0
  141. data/spec/internal/app/views/sessions/new.html.erb +14 -0
  142. data/spec/internal/config/database.yml +3 -0
  143. data/spec/internal/config/initializers/assethost_config.rb +57 -0
  144. data/spec/internal/config/routes.rb +7 -0
  145. data/spec/internal/db/combustion_test.sqlite +0 -0
  146. data/spec/internal/db/schema.rb +106 -0
  147. data/spec/internal/log/test.log +14769 -0
  148. data/spec/internal/public/favicon.ico +0 -0
  149. data/spec/internal/public/images/1_27f7745237849975ca90591c1fba5934_original. +0 -0
  150. data/spec/internal/public/images/1_7d33319deca787d5bb3f62ff06563ad2_original. +0 -0
  151. data/spec/internal/public/images/1_b6d48c8b1286104ce76649731e09645f_original. +0 -0
  152. data/spec/internal/public/images/1_b6d48c8b1286104ce76649731e09645f_original.jpg +0 -0
  153. data/spec/internal/public/images/1_b6d48c8b1286104ce76649731e09645f_original.txt +0 -0
  154. data/spec/internal/public/images/1_e179cbd27e07cb55042d0db36cdac095_original. +0 -0
  155. data/spec/internal/public/images/1_e669edd3dfd74be66fc38416e82e3a37_original. +0 -0
  156. data/spec/lib/asset_host_core/loaders/asset_host_spec.rb +33 -0
  157. data/spec/lib/asset_host_core/loaders/brightcove_spec.rb +51 -0
  158. data/spec/lib/asset_host_core/loaders/flickr_spec.rb +72 -0
  159. data/spec/lib/asset_host_core/loaders/url_spec.rb +42 -0
  160. data/spec/lib/asset_host_core/loaders/vimeo_spec.rb +51 -0
  161. data/spec/lib/asset_host_core/loaders/youtube_spec.rb +73 -0
  162. data/spec/lib/asset_host_core/loaders_spec.rb +4 -0
  163. data/spec/lib/asset_host_core/model_methods_spec.rb +4 -0
  164. data/spec/lib/asset_host_core/paperclip/asset_thumbnail_spec.rb +4 -0
  165. data/spec/lib/asset_host_core/paperclip/attachment_spec.rb +4 -0
  166. data/spec/lib/asset_host_core/resque_job_spec.rb +4 -0
  167. data/spec/lib/asset_host_core_spec.rb +4 -0
  168. data/spec/models/api_user_spec.rb +58 -0
  169. data/spec/models/asset_output_spec.rb +4 -0
  170. data/spec/models/asset_spec.rb +4 -0
  171. data/spec/models/output_spec.rb +4 -0
  172. data/spec/models/permission_spec.rb +4 -0
  173. data/spec/spec_helper.rb +30 -0
  174. data/spec/support/fixture_loader.rb +9 -0
  175. data/spec/support/param_helper.rb +14 -0
  176. data/spec/support/permission_matcher.rb +17 -0
  177. data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  178. data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  179. data/vendor/assets/images/jquery-ui/ui-bg_flat_10_000000_40x100.png +0 -0
  180. data/vendor/assets/images/jquery-ui/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  181. data/vendor/assets/images/jquery-ui/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  182. data/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png +0 -0
  183. data/vendor/assets/images/jquery-ui/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  184. data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  185. data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  186. data/vendor/assets/images/jquery-ui/ui-icons_222222_256x240.png +0 -0
  187. data/vendor/assets/images/jquery-ui/ui-icons_228ef1_256x240.png +0 -0
  188. data/vendor/assets/images/jquery-ui/ui-icons_ef8c08_256x240.png +0 -0
  189. data/vendor/assets/images/jquery-ui/ui-icons_ffd27a_256x240.png +0 -0
  190. data/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png +0 -0
  191. data/vendor/assets/javascripts/backbone.js +1158 -0
  192. data/vendor/assets/javascripts/backbone.modelbinding.js +475 -0
  193. data/vendor/assets/javascripts/exif.js +695 -0
  194. data/vendor/assets/javascripts/jquery-ui.js +5614 -0
  195. data/vendor/assets/javascripts/simplemodal.js +698 -0
  196. data/vendor/assets/javascripts/spin.jquery.js +81 -0
  197. data/vendor/assets/javascripts/spin.min.js +1 -0
  198. data/vendor/assets/javascripts/underscore.min.js +1 -0
  199. metadata +658 -0
@@ -0,0 +1,29 @@
1
+ #= require ./assethost
2
+ #= require underscore.min
3
+
4
+ #= require spin.jquery
5
+ #= require spin.min
6
+
7
+ #= require_self
8
+ #= require_directory ./clients/templates
9
+ #= require_directory ./clients
10
+
11
+ class AssetHost.Client
12
+ DefaultOptions:
13
+ attr: "data-assethost"
14
+
15
+ constructor: (options={}) ->
16
+ @options = _.defaults options, @DefaultOptions
17
+ @clients = []
18
+
19
+ clients = @clients
20
+
21
+ $ =>
22
+ ahAttr = @options.attr
23
+
24
+ # find all assethost elements and look for rich functionality
25
+ $("img[#{@options.attr}]").each ->
26
+ rich = $(this).attr ahAttr
27
+
28
+ if Client[rich]
29
+ clients.push new Client[rich](this)
@@ -0,0 +1,64 @@
1
+ window.BrightcoveVideos ?= {}
2
+
3
+ window.onTemplateLoaded = (id) ->
4
+ @player = brightcove.api.getExperience(id)
5
+ @modVP = @player.getModule(brightcove.api.modules.APIModules.VIDEO_PLAYER)
6
+
7
+ window.onTemplateReady = (event) ->
8
+ @BrightcoveVideos[@player.id].swap()
9
+
10
+
11
+
12
+ class AssetHost.Client.BrightcoveVideo
13
+ DefaultOptions:
14
+ playerKey: "AQ~~,AAAAmtVKbGE~,pW41hkPiaos27C7knwyeOWQgVlG4w7v5"
15
+ playerId: "1247178207001"
16
+ brightcoveJS: "http://admin.brightcove.com/js/BrightcoveExperiences.js"
17
+
18
+ template: JST['asset_host_core/clients/templates/brightcove_embed']
19
+
20
+ constructor: (el, options={}) ->
21
+ @opts = _.defaults options, @DefaultOptions
22
+ @el = $(el) # The asset
23
+
24
+ # we're given an img element. we'll stick an overlay with a play
25
+ # button on it, and then on click we'll launch the video
26
+
27
+ # get width and height from the img
28
+ @w = $(el).attr("width")
29
+ @h = $(el).attr("height")
30
+
31
+ # get videoid from data-ah-videoid attribute
32
+ @videoid = @el.attr("data-ah-videoid")
33
+
34
+ $(document).ready =>
35
+ @launch()
36
+
37
+ #----------
38
+
39
+ launch: ->
40
+ @el.parent().spin(color: "#fff", shadow: true)
41
+
42
+ # render template
43
+ @video = $ @template(
44
+ width: @w
45
+ height: @h
46
+ videoid: @videoid
47
+ playerid: @opts.playerId
48
+ playerkey: @opts.playerKey
49
+ )
50
+
51
+ window.BrightcoveVideos[$("object", @video).attr('id')] = @
52
+
53
+ @el.after @video
54
+
55
+ if window.brightcove?
56
+ brightcove.createExperiences()
57
+ else
58
+ $.getScript @opts.brightcoveJS, ->
59
+ brightcove.createExperiences()
60
+
61
+ swap: ->
62
+ @el.parent().spin(false)
63
+ @el.hide()
64
+ @video.show()
@@ -0,0 +1,18 @@
1
+ <div class="brightcove-video">
2
+ <object id="ah_bcove_<%= @videoid %>" class="BrightcoveExperience">
3
+ <param name="bgcolor" value="#FFFFFF" />
4
+ <param name="width" value="<%= @width %>" />
5
+ <param name="height" value="<%= @height %>" />
6
+ <param name="playerID" value="<%= @playerid %>" />
7
+ <param name="playerKey" value="<%= @playerkey %>" />
8
+ <param name="isVid" value="true" />
9
+ <param name="isUI" value="true" />
10
+ <param name="dynamicStreaming" value="true" />
11
+ <param name="@videoPlayer" value="<%= @videoid %>" />
12
+ <param name="wmode" value="transparent" />
13
+ <param name="autoStart" value ="false" />
14
+ <param name="includeAPI" value="true" />
15
+ <param name="templateLoadHandler" value="onTemplateLoaded" />
16
+ <param name="templateReadyHandler" value="onTemplateReady" />
17
+ </object>
18
+ </div>
@@ -0,0 +1 @@
1
+ <iframe src="http://player.vimeo.com/video/<%=@videoid%>?portrait=0&amp;byline=0" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>
@@ -0,0 +1 @@
1
+ <iframe type='text/html' src='http://www.youtube.com/embed/<%=@videoid%>' frameborder='0' allowfullscreen='true' />
@@ -0,0 +1,21 @@
1
+ class AssetHost.Client.VimeoVideo
2
+ template: JST['asset_host_core/clients/templates/vimeo_embed']
3
+
4
+ constructor: (el, options={}) ->
5
+ @el = $(el)
6
+
7
+ # get videoid from data-ah-videoid attribute
8
+ @videoid = @el.attr("data-ah-videoid")
9
+
10
+ $(document).ready =>
11
+ @launch()
12
+
13
+ #----------
14
+
15
+ launch: ->
16
+ # render template
17
+ @html = @template(videoid: @videoid)
18
+ @swap()
19
+
20
+ swap: ->
21
+ @el.replaceWith @html
@@ -0,0 +1,21 @@
1
+ class AssetHost.Client.YoutubeVideo
2
+ template: JST['asset_host_core/clients/templates/youtube_embed']
3
+
4
+ constructor: (el, options={}) ->
5
+ @el = $(el)
6
+
7
+ # get videoid from data-ah-videoid attribute
8
+ @videoid = @el.attr("data-ah-videoid")
9
+
10
+ $(document).ready =>
11
+ @launch()
12
+
13
+ #----------
14
+
15
+ launch: ->
16
+ # render template
17
+ @html = @template(videoid: @videoid)
18
+ @swap()
19
+
20
+ swap: ->
21
+ @el.replaceWith @html
@@ -0,0 +1,235 @@
1
+ class AssetHost.CMSPlugin
2
+ DefaultOptions:
3
+ {
4
+ el: "",
5
+ server: '',
6
+ assets: [],
7
+ token: ''
8
+ }
9
+
10
+ #----------
11
+
12
+ constructor: (options) ->
13
+ @options = _(_({}).extend(this.DefaultOptions)).extend( options || {} )
14
+
15
+ # add in events
16
+ _.extend(this, Backbone.Events)
17
+
18
+ # cache values for extras
19
+ _(@options.extras).each (v,k) =>
20
+ if v and el = $( '#'+v )[0]
21
+ @options.extras[k] = el.value
22
+
23
+ # store existing form row info
24
+ @rows = []
25
+
26
+ # -- assemble our asset list from form data -- #
27
+ assetdata = []
28
+ for idx in _.range(@options.begins_with,100)
29
+ if el = $( _.template("#"+@options.assetID,{idx:idx,field:@options.id}) )[0]
30
+ asset = {
31
+ id: el.value,
32
+ caption: $( _.template("#"+@options.assetID,{idx:idx,field:@options.caption}) )[0].value,
33
+ #ORDER: $(_.template("#"+@options.assetID,{idx:idx,field:@options.order}) )[0].value
34
+ ORDER: idx
35
+ }
36
+
37
+ # stash row info
38
+ @rows.push {
39
+ idx: idx,
40
+ id: asset.id,
41
+ extras: _(@options.extras).map (v,field) =>
42
+ el = $( _.template("#"+@options.assetID,{idx:idx,field:field}) )[0]
43
+ { id: el.id, name: el.name, value: el.value }
44
+ }
45
+
46
+ assetdata.push(asset)
47
+ else
48
+ # nothing with this index, so go ahead and break
49
+ break
50
+
51
+ # -- load assets -- #
52
+
53
+ @assets = new AssetHost.Models.Assets(assetdata)
54
+
55
+ # load other asset data (tags, credit, etc)
56
+ @assets.each (a,idx) -> a.fetch({success: (a) => a.set({caption:assetdata[ idx ].caption});console.log("set caption to ",assetdata[ idx ].caption)})
57
+
58
+ # -- clone any hidden inputs -- #
59
+
60
+ @hiddens = $(@options.el).find("input[type=hidden]")
61
+
62
+ # -- initialize our views -- #
63
+
64
+ @assetsView = new AssetHost.CMSPlugin.CMSAssets({collection:@assets,args:@options,rows:@rows,hiddens:@hiddens})
65
+ $(@options.el).html @assetsView.el
66
+
67
+ window.addEventListener "message", (evt) =>
68
+ if evt.data != "LOADED"
69
+ found = {}
70
+
71
+ # reconsile our asset list to the returned list
72
+ _(evt.data).each (a,i) =>
73
+ # do we have this asset?
74
+ if asset = @assets.get(a.id)
75
+ # yes... check for changed caption
76
+ asset.set({caption:a.caption,ORDER:i})
77
+ else
78
+ # no, needs to be added
79
+ asset = new AssetHost.Models.Asset(a)
80
+ asset.fetch(success: (aobj)=>aobj.set({caption:a.caption,ORDER:i});@assets.add(aobj))
81
+
82
+ found[ a.id ] = true
83
+
84
+ # now check for removed assets
85
+ remove = []
86
+ @assets.each (a,i) =>
87
+ if found[a.get('id')]
88
+ # we're cool
89
+ else
90
+ # not in our return list... delete
91
+ remove.push(a)
92
+
93
+ for a in remove
94
+ @assets.remove(a)
95
+
96
+ @assets.sort()
97
+ @assetsView.render()
98
+
99
+ @trigger("assets",@assets.toJSON())
100
+ , false
101
+
102
+ #----------
103
+
104
+ @CMSAsset: Backbone.View.extend
105
+ tagName: "li"
106
+
107
+ template:
108
+ '''
109
+ <%= asset.tags ? asset.tags.thumb : "INVALID" %>
110
+ <b><%= asset.title %> (<%= asset.size %>)</b>
111
+ <p><%= asset.caption %></p>
112
+ <input type="hidden" id="<%= id.id %>" name="<%= id.name %>" value="<%= asset.id %>" />
113
+ <input type="hidden" id="<%= caption.id %>" name="<%= caption.name %>" value="<%= (asset.caption||"").replace(/"/g,'&quot;') %>" />
114
+ <input type="hidden" id="<%= order.id %>" name="<%= order.name %>" value="<%= idx+1 %>" />
115
+ <% _(extras).each(function(ex) { %>
116
+ <input type="hidden" id="<%= ex.id %>" name="<%= ex.name %>" value="<%= ex.value %>" />
117
+ <% }); %>
118
+ '''
119
+
120
+ initialize: ->
121
+ # if we get an invalid asset, remove it
122
+ if !@model.get("id")
123
+ @model.collection.remove(@model)
124
+ return false
125
+
126
+ $(@el).attr("data-asset-url",@model.get('url'))
127
+ @render()
128
+ @model.bind "change", => @render()
129
+
130
+ render: ->
131
+
132
+ if @model.get('tags')
133
+ idx = @model.get('ORDER')
134
+ #idx = @model.collection.indexOf(@model)
135
+
136
+ if @options.rows[idx]
137
+ extras = @options.rows[idx].extras
138
+ else
139
+ extras = _(@options.args.extras).map (v,k) => {
140
+ id: _.template(@options.args.assetID,{idx:idx,field:k}),
141
+ name: _.template(@options.args.assetName,{idx:idx,field:k}),
142
+ value: v
143
+ }
144
+
145
+ $( @el ).html( _.template @template, {
146
+ asset: @model.toJSON(),
147
+ idx: idx,
148
+ id: {
149
+ id: _.template(@options.args.assetID,{idx:idx,field:@options.args.id}),
150
+ name: _.template(@options.args.assetName,{idx:idx,field:@options.args.id})
151
+ },
152
+ caption: {
153
+ id: _.template(@options.args.assetID,{idx:idx,field:@options.args.caption}),
154
+ name: _.template(@options.args.assetName,{idx:idx,field:@options.args.caption})
155
+ },
156
+ order: {
157
+ id: _.template(@options.args.assetID,{idx:idx,field:@options.args.order}),
158
+ name: _.template(@options.args.assetName,{idx:idx,field:@options.args.order})
159
+ },
160
+ extras: extras
161
+ } )
162
+
163
+ return this
164
+
165
+ #----------
166
+
167
+ @CMSAssets: Backbone.View.extend
168
+ tagName: "ul"
169
+ events: { "click button": "_popup" }
170
+
171
+ initialize: ->
172
+ @_views = {}
173
+ @collection.bind "reset", =>
174
+ _(@_views).each (a) => $(a.el).detach(); @_views = {}
175
+
176
+ @collection.bind 'add', (f) =>
177
+ @_views[f.cid] = new AssetHost.CMSPlugin.CMSAsset({model:f,args:@options.args,rows:@options.rows})
178
+ @render()
179
+
180
+ @collection.bind 'remove', (f) =>
181
+ if @_views[f.cid]
182
+ @_views[f.cid].remove()
183
+ delete @_views[f.cid]
184
+
185
+ @render()
186
+
187
+ # now that all our events are up, render
188
+ @render()
189
+
190
+ _popup: (evt) ->
191
+ evt.originalEvent.stopPropagation()
192
+ evt.originalEvent.preventDefault()
193
+ newwindow = window.open("http://#{AssetHost.SERVER}/a/chooser", 'chooser', 'height=620,width=1000,scrollbars=1')
194
+
195
+ # attach a listener to wait for the LOADED message
196
+ window.addEventListener "message", (evt) =>
197
+ if evt.data == "LOADED"
198
+ # dispatch our event with the asset data
199
+ newwindow.postMessage @collection.toJSON(), "http://#{AssetHost.SERVER}"
200
+ , false
201
+
202
+ return false
203
+
204
+ render: ->
205
+ @collection.each (a) =>
206
+ @_views[a.cid] ?= new AssetHost.CMSPlugin.CMSAsset({model:a,args:@options.args,rows:@options.rows})
207
+
208
+ views = _(@_views).sortBy (a) => a.model.get("ORDER")
209
+ $(@el).html( _(views).map (v) -> v.el )
210
+
211
+ # add hiddens
212
+ #$(@el).append(@options.hiddens)
213
+
214
+ # we need to render any removed rows as empty, with extras and possibly DELETE
215
+ if (@collection.length < @options.rows.length)
216
+ for idx in _.range(@collection.length,@options.rows.length)
217
+ _(@options.rows[idx].extras).each( (ex) =>
218
+ $( @el ).append $("<input/>",{type:'hidden',name:ex.name,id:ex.id,value:ex.value})
219
+ )
220
+
221
+ if @options.args.delete
222
+ $( @el ).append $("<input/>",{
223
+ type: "hidden",
224
+ id: _.template(@options.args.assetID,{idx:idx,field:@options.args.delete}),
225
+ name: _.template(@options.args.assetName,{idx:idx,field:@options.args.delete}),
226
+ value: "on"
227
+ })
228
+
229
+ $(@el).append( $("<li/>").html( $('<button/>',{text:"Pop Up Asset Chooser"})))
230
+
231
+ if @options.args.count
232
+ if (el = $('#'+@options.args.count)[0]) and @collection.length > @options.rows.length
233
+ el.value = @collection.length
234
+
235
+ return this
@@ -0,0 +1,586 @@
1
+ class AssetHost.Models
2
+ constructor: ->
3
+
4
+ class @Asset extends Backbone.Model
5
+ urlRoot: "http://#{AssetHost.SERVER}#{AssetHost.PATH_PREFIX}/api/assets/"
6
+
7
+ modal: ->
8
+ @_modal ?= new AssetHost.Models.AssetModalView(model: @)
9
+
10
+ #----------
11
+
12
+ url: ->
13
+ url = if @isNew() then @urlRoot else @urlRoot + encodeURIComponent(@id)
14
+
15
+ if AssetHost.TOKEN
16
+ url = url + "?" + $.param({auth_token:AssetHost.TOKEN})
17
+
18
+ url
19
+
20
+ #----------
21
+
22
+ chopCaption: (count=100) ->
23
+ chopped = @get('caption')
24
+
25
+ if chopped and chopped.length > count
26
+ regstr = "^(.{#{count}}\\w*)\\W"
27
+ chopped = chopped.match(new RegExp(regstr))
28
+
29
+ if chopped
30
+ chopped = "#{chopped[1]}..."
31
+ else
32
+ chopped = @get('caption')
33
+
34
+ chopped
35
+
36
+ #----------
37
+
38
+ class @Assets extends Backbone.Collection
39
+ baseUrl: "#{AssetHost.PATH_PREFIX}/api/assets",
40
+ model: Models.Asset
41
+
42
+ # If we have an ORDER attribute, sort by that. Otherwise, sort by just
43
+ # the asset ID.
44
+ comparator: (asset) ->
45
+ asset.get("ORDER") || -Number(asset.get("id"))
46
+
47
+ #----------
48
+
49
+
50
+ class @PaginatedAssets extends @Assets
51
+ initialize: ->
52
+ _.bindAll(this, 'parse', 'url')
53
+
54
+ @_page = 1
55
+ @_query = ''
56
+ @per_page = 24
57
+ @total_entries = 0
58
+
59
+ @
60
+
61
+ parse: (resp, xhr) ->
62
+ @next_page = xhr.getResponseHeader('X-Next-Page')
63
+ @total_entries = xhr.getResponseHeader('X-Total-Entries')
64
+
65
+ resp
66
+
67
+ url: ->
68
+ @baseUrl + "?" + $.param(page: @_page, q: @_query)
69
+
70
+ query: (q=@_query) ->
71
+ @_query = q if q?
72
+ @_query
73
+
74
+ page: (p=null) ->
75
+ @_page = Number(p) if p? && p != ''
76
+ @_page
77
+
78
+ #----------
79
+
80
+ class @AssetDropAssetView extends Backbone.View
81
+ tagName: 'li'
82
+ template: JST['asset_host_core/templates/asset_drop_asset']
83
+ events:
84
+ 'click button.delete': "_remove"
85
+ 'click': '_click'
86
+
87
+ #----------
88
+
89
+ initialize: ->
90
+ @del_confirm = false
91
+ @del_timeout = null
92
+
93
+ @drop = @options.drop
94
+ @model.bind "change", => @render()
95
+ @render()
96
+
97
+ #----------
98
+
99
+ _remove: (evt) ->
100
+ if @del_confirm
101
+ # delete
102
+ clearTimeout @del_timeout
103
+
104
+ # remove our model...
105
+ _.defer => @drop.trigger 'remove', @model
106
+ else
107
+ target = $(evt.target)
108
+ target.text "Really Delete?"
109
+ @del_confirm = true
110
+
111
+ # set a reset timeout
112
+ @del_timeout = setTimeout =>
113
+ target.text "x"
114
+ @del_confirm = false
115
+ @del_timeout = null
116
+ , 2000
117
+
118
+ false
119
+
120
+ #----------
121
+
122
+ _click: (evt) ->
123
+ if not $(evt.currentTarget).hasClass("delete")
124
+ @drop.trigger 'click', @model
125
+
126
+ #----------
127
+
128
+ render: ->
129
+ $(@el).html @template
130
+ asset: @model.toJSON()
131
+ chop: @model.chopCaption()
132
+
133
+ $(@el).attr "data-asset-id", @model.get("id")
134
+ @
135
+
136
+ #----------
137
+
138
+ class @AssetDropView extends Backbone.View
139
+ tagName: "ul"
140
+ className: "assets"
141
+
142
+ initialize: ->
143
+ @_views = {}
144
+
145
+ @collection.bind 'add', (f) =>
146
+ @collection.sort()
147
+
148
+ @collection.bind 'remove', (f) =>
149
+ @collection.sort()
150
+
151
+ @collection.bind 'reset', (f) =>
152
+ _(@_views).each (av) => $(av.el).detach()
153
+ @_views = {}
154
+ @render()
155
+
156
+ #----------
157
+
158
+ render: ->
159
+ # set up views for each collection member
160
+ @collection.each (f) =>
161
+ # create a view unless one exists
162
+ @_views[f.cid] ?= new Models.AssetDropAssetView(model: f, drop: @)
163
+
164
+ # make sure all of our view elements are added
165
+ $(@el).append( _(@_views).map (v) -> v.el )
166
+
167
+ $(@el).sortable
168
+ update: (evt,ui) =>
169
+ _(@el.children).each (li,idx) =>
170
+ id = $(li).attr('data-asset-id')
171
+ @collection.get(id).attributes.ORDER = idx
172
+ @collection.sort()
173
+
174
+ @
175
+
176
+ #----------
177
+
178
+ class @AssetSearchView extends Backbone.View
179
+ className: "search_box"
180
+ template: JST['asset_host_core/templates/asset_search']
181
+ events:
182
+ 'click button': 'search',
183
+ 'keypress input:text': '_keypress'
184
+
185
+ initialize: ->
186
+ @collection.bind('all', => @render() )
187
+
188
+ _keypress: (e) ->
189
+ @search() if e.which == 13
190
+
191
+ search: ->
192
+ query = $(@el).find("input")[0].value
193
+ @trigger "search", query
194
+
195
+ render: ->
196
+ $(@el).html @template(query: @collection.query())
197
+ @
198
+
199
+ #----------
200
+
201
+ class @AssetBrowserAssetView extends Backbone.View
202
+ tagName: "li"
203
+ template: JST['asset_host_core/templates/browser_asset']
204
+ tipTemplate: JST['asset_host_core/templates/browser_asset_tip']
205
+
206
+ initialize: ->
207
+ @id = "ab_#{@model.get('id')}"
208
+ $(@el).attr("data-asset-url",@model.get('url'))
209
+
210
+ @render()
211
+
212
+ $(@el).find('button')[0].addEventListener "click", (evt) =>
213
+ @trigger "click", @model
214
+ true
215
+
216
+ # add tooltip
217
+ $(@el).tooltip
218
+ title: @tipTemplate(@model.toJSON())
219
+ html: true
220
+
221
+ @model.bind "change", => @render()
222
+
223
+ render: ->
224
+ $(@el).html @template(@model.toJSON())
225
+ $(@el).attr "draggable", true
226
+ @
227
+
228
+ #----------
229
+
230
+ class @AssetBrowserView extends Backbone.View
231
+ tagName: "ul"
232
+
233
+ initialize: ->
234
+ @_views = {}
235
+
236
+ @container = $("#content_right")
237
+
238
+ @collection.bind "reset", =>
239
+ _(@_views).each (a) => $(a.el).detach()
240
+ @_views = {}
241
+ @render()
242
+
243
+ pages: ->
244
+ @_pages ?= (new AssetHost.Models.PaginationLinks(@collection)).render()
245
+
246
+ loading: ->
247
+ $(@el).css(opacity: ".1")
248
+ @container.spin()
249
+
250
+ doneLoading: ->
251
+ $(@el).css(opacity: "1")
252
+ @container.spin(false)
253
+
254
+ render: ->
255
+ # set up views for each collection member
256
+ @collection.each (a) =>
257
+ # create a view unless one exists
258
+ @_views[a.cid] ?= new AssetHost.Models.AssetBrowserAssetView(model: a)
259
+ @_views[a.cid].bind "click", (a) => @trigger "click", a
260
+
261
+ # make sure all of our view elements are added
262
+ $(@el).append( _(@_views).map (v) -> v.el )
263
+
264
+ # clear loading status
265
+ @doneLoading()
266
+ @
267
+
268
+ #----------
269
+
270
+ class @AssetModalView extends Backbone.View
271
+ className: "modal"
272
+ events:
273
+ 'click a.select': '_select'
274
+ 'click a.admin': '_admin'
275
+ 'click a.close': 'close'
276
+
277
+ template: JST['asset_host_core/templates/asset_modal']
278
+
279
+ open: (options) ->
280
+ @options = options || {}
281
+ $(@render().el).modal()
282
+
283
+ $(@render().el).on "hide", => @options.close?()
284
+
285
+ close: ->
286
+ $(@el).modal('hide')
287
+
288
+ _select: ->
289
+ @close()
290
+ @model.trigger('selected',@model)
291
+
292
+ _admin: ->
293
+ @close()
294
+ @model.trigger('admin',@model)
295
+
296
+ render: ->
297
+ $(@el).html @template
298
+ asset: @model.toJSON()
299
+ select: if @options.select? then @options.select else true
300
+ admin: if @options.admin? then @options.admin else false
301
+
302
+ @
303
+
304
+ #----------
305
+
306
+ class @SaveAndCloseView extends Backbone.View
307
+ events: 'click button': 'saveAndClose'
308
+ template: JST['asset_host_core/templates/save_and_close_view']
309
+
310
+ initialize: ->
311
+ @collection.bind "all", => @render()
312
+ @render()
313
+
314
+ saveAndClose: ->
315
+ # make sure collection is sorted before we return it
316
+ @collection.sort()
317
+ @trigger 'saveAndClose', @collection.toJSON()
318
+
319
+ render: ->
320
+ $(@el).html @template(count: @collection.size())
321
+ @
322
+
323
+ #----------
324
+
325
+ class @PaginationLinks extends Backbone.View
326
+ className: "pagination pagination-centered"
327
+ template: JST['asset_host_core/templates/pagination_links']
328
+ linkTemplate: JST['asset_host_core/templates/pagination_link']
329
+
330
+ DefaultOptions:
331
+ inner_window: 3,
332
+ outer_window: 1,
333
+ prev_label: "&#8592;",
334
+ next_label: "&#8594;",
335
+ separator: " ",
336
+ spacer: "<li class='disabled'><a href='#'>...</a></li>"
337
+
338
+ events: 'click li': 'clickPage'
339
+
340
+ initialize: (@collection, options={}) ->
341
+ @options = _.defaults options, @DefaultOptions
342
+
343
+ @collection.bind "reset", => @render()
344
+ @collection.bind "add", => @render()
345
+ @collection.bind "change", => @render()
346
+
347
+ #----------
348
+
349
+ clickPage: (evt) ->
350
+ page = $(evt.currentTarget).attr("data-page")
351
+
352
+ if page
353
+ @trigger "page", page
354
+
355
+ render: ->
356
+ # what pages are we displaying?
357
+ pages = Math.floor( @collection.total_entries / @collection.per_page + 1)
358
+ current = @collection._page
359
+
360
+ rendered = {}
361
+ links = []
362
+
363
+ # start with outer_window from 1
364
+ _(_.range(1,1+@options.outer_window)).each (i) =>
365
+ links.push @linkTemplate
366
+ page: i
367
+ current: current==i
368
+
369
+
370
+ rendered[ i ] = true
371
+
372
+
373
+ # now try -inner_window from current
374
+ _(_.range(current-@options.inner_window,current)).each (i) =>
375
+ if i > 0 && !rendered[ i ]
376
+ if i-1 > 0 && !rendered[i-1]
377
+ links.push @options.spacer
378
+
379
+ links.push @linkTemplate
380
+ page: i
381
+ current: false
382
+
383
+
384
+ rendered[ i ] = true
385
+
386
+
387
+ # now try current
388
+ if !rendered[ current ]
389
+ if current-1 > 0 && !rendered[current-1]
390
+ links.push @options.spacer
391
+
392
+ links.push @linkTemplate
393
+ page: current
394
+ current: true
395
+
396
+
397
+ rendered[ current ] = true
398
+
399
+ # now try +inner_window from current
400
+ _(_.range(current+1,current+@options.inner_window+1)).each (i) =>
401
+ if i < pages && !rendered[ i ]
402
+ if i-1 > 0 && !rendered[i-1]
403
+ links.push @options.spacer
404
+
405
+ links.push @linkTemplate
406
+ page: i
407
+ current: false
408
+
409
+
410
+ rendered[ i ] = true
411
+
412
+
413
+ # and finally, -outer_window from last page
414
+ _(_.range(pages+1-@options.outer_window,pages+1)).each (i) =>
415
+ if i > 0 && !rendered[ i ]
416
+ if i-1 > 0 && !rendered[i-1]
417
+ links.push @options.spacer
418
+
419
+ links.push @linkTemplate
420
+ page: i
421
+ collection: @collection
422
+ current: current==i
423
+
424
+
425
+ rendered[ i ] = true
426
+
427
+
428
+ $(@el).html @template
429
+ current: current
430
+ pages: pages
431
+ links: links.join(@options.separator)
432
+ options: @options
433
+
434
+
435
+ @
436
+
437
+ #----------
438
+
439
+ @queuedSync: (method,model,success,error) ->
440
+ #
441
+
442
+ class @QueuedFile extends Backbone.Model
443
+ sync: Models.queuedSync
444
+
445
+ upload: ->
446
+ return false if @xhr
447
+
448
+ @xhr = new XMLHttpRequest
449
+
450
+ $(@xhr.upload).bind "progress", (evt) =>
451
+ evt = evt.originalEvent
452
+ @set {"PERCENT": if evt.lengthComputable then Math.floor(evt.loaded/evt.total*100) else evt.loaded}
453
+
454
+ $(@xhr.upload).bind "complete", (evt) =>
455
+ @set {"STATUS": "pending"}
456
+
457
+ @xhr.onreadystatechange = (req) =>
458
+ if @xhr.readyState == 4 && @xhr.status == 200
459
+ @set {"STATUS": "complete"}
460
+
461
+ if req.responseText != "ERROR"
462
+ @set {"ASSET": $.parseJSON(@xhr.responseText)}
463
+ @trigger "uploaded", this
464
+
465
+ @xhr.open('POST',this.collection.urlRoot, true)
466
+ @xhr.setRequestHeader('X_FILE_NAME', @get('file').name)
467
+ @xhr.setRequestHeader('CONTENT_TYPE', @get('file').type)
468
+ @xhr.setRequestHeader('HTTP_X_FILE_UPLOAD','true')
469
+
470
+ # and away we go...
471
+ @xhr.send @get('file')
472
+ @set {"STATUS": "uploading"}
473
+
474
+ readableSize: ->
475
+ return false if !@get('size')
476
+ size = @get('size')
477
+
478
+ units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
479
+ i = 0;
480
+
481
+ while size >= 1024
482
+ size /= 1024
483
+ ++i
484
+
485
+ size.toFixed(1) + ' ' + units[i];
486
+
487
+ #----------
488
+
489
+ class @QueuedFiles extends Backbone.Collection
490
+ model: Models.QueuedFile
491
+ urlRoot: "#{AssetHost.PATH_PREFIX}/a/assets/upload"
492
+
493
+ initialize: (models,options) ->
494
+ @urlRoot = options.urlRoot
495
+
496
+ #----------
497
+
498
+ class @QueuedFileView extends Backbone.View
499
+ events:
500
+ 'click button.remove': '_remove',
501
+ 'click button.upload': '_upload'
502
+
503
+ tagName: "li"
504
+ template: JST['asset_host_core/templates/queued_file']
505
+
506
+ initialize: ->
507
+ @model.bind "change", => @render()
508
+
509
+ @img = ''
510
+
511
+ # try to read file on disk
512
+ file = @model.get('file')
513
+ if file.type.match('image.*')
514
+ reader = new FileReader()
515
+
516
+ reader.onload = (e) =>
517
+ @img = $ "<img/>", {
518
+ class: "thumb",
519
+ src: e.target.result,
520
+ title: file.name
521
+ }
522
+
523
+ m = /^([^,]+),(.*)$/.exec(e.target.result)
524
+ @exif = EXIF.readFromBinaryFile(window.atob(m[2]))
525
+
526
+ @render()
527
+
528
+ reader.readAsDataURL(file)
529
+
530
+ @render()
531
+
532
+ _remove: (evt) ->
533
+ @model.collection.remove(@model)
534
+
535
+ _upload: (evt) ->
536
+ @model.upload()
537
+
538
+ render: ->
539
+ $(@el).attr('class',@model.get("STATUS"))
540
+
541
+ $(@el).html @template
542
+ exif: @exif
543
+ name: @model.get('name')
544
+ size: @model.readableSize()
545
+ STATUS: @model.get('STATUS')
546
+ PERCENT: @model.get('PERCENT')
547
+ xhr: if @model.xhr then true else false
548
+
549
+
550
+ $(@el).prepend(@img) if @img
551
+ @
552
+
553
+ #----------
554
+
555
+ class @QueuedFilesView extends Backbone.View
556
+ tagName: "ul"
557
+ className: "uploads"
558
+
559
+ initialize: ->
560
+ @_views = {}
561
+
562
+ @collection.bind 'add', (f) =>
563
+ @_views[f.cid] = new Models.QueuedFileView(model: f)
564
+ @render()
565
+
566
+ @collection.bind 'remove', (f) =>
567
+ $(@_views[f.cid].el).detach()
568
+ delete @_views[f.cid]
569
+ @render()
570
+
571
+ @collection.bind 'reset', (f) =>
572
+ @_views = {}
573
+
574
+ _reset: (f) ->
575
+ # do we need this?
576
+
577
+ render: ->
578
+ # set up views for each collection member
579
+ @collection.each (f) =>
580
+ # create a view unless one exists
581
+ @_views[f.cid] ?= new Models.QueuedFileView(model: f)
582
+
583
+ # make sure all of our view elements are added
584
+ $(@el).append( _(@_views).map (v) -> v.el )
585
+
586
+ @