slices 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (215) hide show
  1. data/CHANGELOG.md +3 -0
  2. data/README.md +51 -0
  3. data/Rakefile +9 -0
  4. data/app/assets/images/slices/ajax-loader.gif +0 -0
  5. data/app/assets/images/slices/asset-background.png +0 -0
  6. data/app/assets/images/slices/asset-spinner.gif +0 -0
  7. data/app/assets/images/slices/bg_header.gif +0 -0
  8. data/app/assets/images/slices/black-Linen.png +0 -0
  9. data/app/assets/images/slices/calendar.svg +68 -0
  10. data/app/assets/images/slices/chosen-sprite.png +0 -0
  11. data/app/assets/images/slices/drag-handle.svg +9 -0
  12. data/app/assets/images/slices/icon_admins.png +0 -0
  13. data/app/assets/images/slices/icon_app.png +0 -0
  14. data/app/assets/images/slices/icon_assets.png +0 -0
  15. data/app/assets/images/slices/icon_collapse.png +0 -0
  16. data/app/assets/images/slices/icon_drag.png +0 -0
  17. data/app/assets/images/slices/icon_files.png +0 -0
  18. data/app/assets/images/slices/icon_generic_file.png +0 -0
  19. data/app/assets/images/slices/icon_images.png +0 -0
  20. data/app/assets/images/slices/icon_padlock.png +0 -0
  21. data/app/assets/images/slices/icon_page.png +0 -0
  22. data/app/assets/images/slices/icon_search.png +0 -0
  23. data/app/assets/images/slices/icon_set-link.png +0 -0
  24. data/app/assets/images/slices/icon_set.png +0 -0
  25. data/app/assets/images/slices/icon_sitemap.png +0 -0
  26. data/app/assets/images/slices/icon_snippets.png +0 -0
  27. data/app/assets/images/slices/icon_template.jpg +0 -0
  28. data/app/assets/images/slices/icon_upload_happy.png +0 -0
  29. data/app/assets/images/slices/icon_upload_sad.png +0 -0
  30. data/app/assets/images/slices/icon_upload_thinking.png +0 -0
  31. data/app/assets/images/slices/noise.png +0 -0
  32. data/app/assets/images/slices/sitemap_icon_ghost.png +0 -0
  33. data/app/assets/images/slices/sitemap_icon_home.png +0 -0
  34. data/app/assets/images/slices/sitemap_icon_page.png +0 -0
  35. data/app/assets/images/slices/sitemap_icon_set_page.png +0 -0
  36. data/app/assets/images/slices/sitemap_icon_virtual_page.png +0 -0
  37. data/app/assets/images/slices/sitemap_overlay.png +0 -0
  38. data/app/assets/images/slices/spinner.gif +0 -0
  39. data/app/assets/images/slices/trash.png +0 -0
  40. data/app/assets/javascripts/admin.js.erb +18 -0
  41. data/app/assets/javascripts/slices/app/backbones/admins.js +114 -0
  42. data/app/assets/javascripts/slices/app/backbones/entries.js +172 -0
  43. data/app/assets/javascripts/slices/app/backbones/generic.js +101 -0
  44. data/app/assets/javascripts/slices/app/backbones/snippets.js +113 -0
  45. data/app/assets/javascripts/slices/app/helpers/assets.js +61 -0
  46. data/app/assets/javascripts/slices/app/helpers/breadcrumbs.js +30 -0
  47. data/app/assets/javascripts/slices/app/helpers/composer.js +26 -0
  48. data/app/assets/javascripts/slices/app/helpers/date_field.js +16 -0
  49. data/app/assets/javascripts/slices/app/helpers/get_value.js +31 -0
  50. data/app/assets/javascripts/slices/app/helpers/icon_upload_names.js.erb +5 -0
  51. data/app/assets/javascripts/slices/app/helpers/layout.js +20 -0
  52. data/app/assets/javascripts/slices/app/helpers/sitemap.js +150 -0
  53. data/app/assets/javascripts/slices/app/helpers/slice_preview.js +48 -0
  54. data/app/assets/javascripts/slices/app/helpers/tagging.js +73 -0
  55. data/app/assets/javascripts/slices/app/helpers/token_field.js +17 -0
  56. data/app/assets/javascripts/slices/app/helpers/upload_icons.js.erb +5 -0
  57. data/app/assets/javascripts/slices/app/helpers/uploader.js +127 -0
  58. data/app/assets/javascripts/slices/app/models/asset.js +29 -0
  59. data/app/assets/javascripts/slices/app/models/asset_collection.js +41 -0
  60. data/app/assets/javascripts/slices/app/models/attachment.js +29 -0
  61. data/app/assets/javascripts/slices/app/models/attachment_collection.js +7 -0
  62. data/app/assets/javascripts/slices/app/models/composer_item.js +1 -0
  63. data/app/assets/javascripts/slices/app/models/composer_item_collection.js +3 -0
  64. data/app/assets/javascripts/slices/app/models/file.js +103 -0
  65. data/app/assets/javascripts/slices/app/models/page.js +186 -0
  66. data/app/assets/javascripts/slices/app/models/s3_file.js +64 -0
  67. data/app/assets/javascripts/slices/app/slices.js +661 -0
  68. data/app/assets/javascripts/slices/app/views/asset_editor_view.js.erb +209 -0
  69. data/app/assets/javascripts/slices/app/views/asset_library_view.js +720 -0
  70. data/app/assets/javascripts/slices/app/views/asset_thumb_view.js.erb +191 -0
  71. data/app/assets/javascripts/slices/app/views/attachment_composer_view.js +350 -0
  72. data/app/assets/javascripts/slices/app/views/attachment_view.js +101 -0
  73. data/app/assets/javascripts/slices/app/views/calendar_view.js +198 -0
  74. data/app/assets/javascripts/slices/app/views/composer_item_view.js +54 -0
  75. data/app/assets/javascripts/slices/app/views/composer_view.js +130 -0
  76. data/app/assets/javascripts/slices/app/views/date_field_view.js +177 -0
  77. data/app/assets/javascripts/slices/app/views/file_view.js +142 -0
  78. data/app/assets/javascripts/slices/app/views/token_field_view.js +253 -0
  79. data/app/assets/javascripts/slices/lib/freeze.js +14 -0
  80. data/app/assets/javascripts/slices/lib/human_file_size.js +16 -0
  81. data/app/assets/javascripts/slices/lib/json_patch.js +9 -0
  82. data/app/assets/javascripts/slices/lib/moment.js +47 -0
  83. data/app/assets/javascripts/slices/lib/plugins.js +101 -0
  84. data/app/assets/javascripts/slices/lib/sortable.js +14 -0
  85. data/app/assets/javascripts/slices/slices.js +27 -0
  86. data/app/assets/javascripts/slices/vendor/autoscroll.js +188 -0
  87. data/app/assets/javascripts/slices/vendor/backbone.js +38 -0
  88. data/app/assets/javascripts/slices/vendor/handlebars.js +1920 -0
  89. data/app/assets/javascripts/slices/vendor/jqmodal.js +69 -0
  90. data/app/assets/javascripts/slices/vendor/jquery-ui.js +274 -0
  91. data/app/assets/javascripts/slices/vendor/jquery-ui_nested-sortable.js +357 -0
  92. data/app/assets/javascripts/slices/vendor/jquery.ajaxprogress.js +76 -0
  93. data/app/assets/javascripts/slices/vendor/jquery.js +2 -0
  94. data/app/assets/javascripts/slices/vendor/livefield.js +459 -0
  95. data/app/assets/javascripts/slices/vendor/moment.js +6 -0
  96. data/app/assets/javascripts/slices/vendor/rails.js +315 -0
  97. data/app/assets/javascripts/slices/vendor/underscore-string.js +1 -0
  98. data/app/assets/javascripts/slices/vendor/underscore.js +5 -0
  99. data/app/assets/stylesheets/admin.css +1 -0
  100. data/app/assets/stylesheets/slices/admin.css.erb +2237 -0
  101. data/app/assets/stylesheets/slices/reset_html5.css +106 -0
  102. data/app/assets/stylesheets/slices/slices.css +7 -0
  103. data/app/controllers/admin/admin_controller.rb +10 -0
  104. data/app/controllers/admin/admins_controller.rb +76 -0
  105. data/app/controllers/admin/assets_controller.rb +53 -0
  106. data/app/controllers/admin/auth/omniauth_callbacks_controller.rb +15 -0
  107. data/app/controllers/admin/auth/passwords_controller.rb +4 -0
  108. data/app/controllers/admin/auth/sessions_controller.rb +4 -0
  109. data/app/controllers/admin/entries_controller.rb +88 -0
  110. data/app/controllers/admin/page_search_controller.rb +12 -0
  111. data/app/controllers/admin/pages_controller.rb +103 -0
  112. data/app/controllers/admin/site_maps_controller.rb +15 -0
  113. data/app/controllers/admin/snippets_controller.rb +33 -0
  114. data/app/controllers/application_controller.rb +4 -0
  115. data/app/controllers/pages_controller.rb +45 -0
  116. data/app/controllers/slices_controller.rb +63 -0
  117. data/app/controllers/static_assets_controller.rb +52 -0
  118. data/app/helpers/admin/admin_helper.rb +63 -0
  119. data/app/helpers/admin/assets_helper.rb +36 -0
  120. data/app/helpers/admin/entries_helper.rb +13 -0
  121. data/app/helpers/admin/site_maps_helper.rb +104 -0
  122. data/app/helpers/assets_helper.rb +64 -0
  123. data/app/helpers/navigation_helper.rb +195 -0
  124. data/app/helpers/pages_helper.rb +119 -0
  125. data/app/models/admin.rb +34 -0
  126. data/app/models/asset.rb +211 -0
  127. data/app/models/attachment.rb +11 -0
  128. data/app/models/layout.rb +44 -0
  129. data/app/models/page.rb +214 -0
  130. data/app/models/placeholder_slice.rb +8 -0
  131. data/app/models/set_page.rb +12 -0
  132. data/app/models/set_slice.rb +57 -0
  133. data/app/models/site_map.rb +24 -0
  134. data/app/models/slice.rb +80 -0
  135. data/app/models/snippet.rb +21 -0
  136. data/app/observers/asset_observer.rb +6 -0
  137. data/app/observers/page_observer.rb +37 -0
  138. data/app/presenters/entry_presenter.rb +17 -0
  139. data/app/presenters/page_presenter.rb +67 -0
  140. data/app/presenters/presenter.rb +9 -0
  141. data/app/presenters/set_page_presenter.rb +2 -0
  142. data/app/views/admin/admins/index.html.erb +26 -0
  143. data/app/views/admin/admins/show.html.erb +27 -0
  144. data/app/views/admin/assets/index.html.erb +1 -0
  145. data/app/views/admin/auth/passwords/edit.html.erb +20 -0
  146. data/app/views/admin/auth/passwords/new.html.erb +14 -0
  147. data/app/views/admin/auth/sessions/_form.html.erb +35 -0
  148. data/app/views/admin/auth/sessions/new.html.erb +14 -0
  149. data/app/views/admin/entries/index.html.erb +32 -0
  150. data/app/views/admin/pages/_breadcrumbs.html.erb +32 -0
  151. data/app/views/admin/pages/_slices.html.erb +27 -0
  152. data/app/views/admin/pages/new.html.erb +14 -0
  153. data/app/views/admin/pages/show.html.erb +50 -0
  154. data/app/views/admin/shared/_asset_storage.html.erb +17 -0
  155. data/app/views/admin/shared/_custom_links.html.erb +1 -0
  156. data/app/views/admin/shared/_custom_navigation.html.erb +1 -0
  157. data/app/views/admin/shared/_navigation.html.erb +5 -0
  158. data/app/views/admin/site_maps/_page_li.html.erb +20 -0
  159. data/app/views/admin/site_maps/_set_page_li.html.erb +23 -0
  160. data/app/views/admin/site_maps/index.html.erb +29 -0
  161. data/app/views/admin/snippets/form.html.erb +12 -0
  162. data/app/views/admin/snippets/index.html.erb +20 -0
  163. data/app/views/admin/snippets/update.html.erb +0 -0
  164. data/app/views/layouts/admin.html.erb +72 -0
  165. data/lib/ext/file_store_cache.rb +18 -0
  166. data/lib/generators/humans/USAGE +8 -0
  167. data/lib/generators/humans/humans_generator.rb +10 -0
  168. data/lib/generators/humans/templates/humans.txt +6 -0
  169. data/lib/generators/slice/USAGE +28 -0
  170. data/lib/generators/slice/slice_generator.rb +123 -0
  171. data/lib/generators/slice/templates/main_fields.hbs +11 -0
  172. data/lib/generators/slice/templates/meta_fields.hbs +11 -0
  173. data/lib/generators/slice/templates/page.rb +19 -0
  174. data/lib/generators/slice/templates/presenter.rb +53 -0
  175. data/lib/generators/slice/templates/set.html.erb +8 -0
  176. data/lib/generators/slice/templates/set_slice.rb +14 -0
  177. data/lib/generators/slice/templates/set_slice_fields.hbs +5 -0
  178. data/lib/generators/slice/templates/show.html.erb +48 -0
  179. data/lib/generators/slice/templates/show_slice.rb +20 -0
  180. data/lib/generators/slice/templates/slice.rb +58 -0
  181. data/lib/generators/slice/templates/slice_fields.hbs +74 -0
  182. data/lib/generators/templates/slices.rb +211 -0
  183. data/lib/mongo_search.rb +84 -0
  184. data/lib/paperclip_validator.rb +5 -0
  185. data/lib/rack_utf8_fix.rb +10 -0
  186. data/lib/sRGB.icc +0 -0
  187. data/lib/set_link_renderer.rb +31 -0
  188. data/lib/slices.rb +68 -0
  189. data/lib/slices/asset/maker.rb +55 -0
  190. data/lib/slices/asset/rename.rb +67 -0
  191. data/lib/slices/available_slices.rb +43 -0
  192. data/lib/slices/cms_form_builder.rb +42 -0
  193. data/lib/slices/config.rb +93 -0
  194. data/lib/slices/container_parser.rb +70 -0
  195. data/lib/slices/generator_macros.rb +36 -0
  196. data/lib/slices/has_attachments.rb +111 -0
  197. data/lib/slices/has_slices.rb +88 -0
  198. data/lib/slices/i18n.rb +6 -0
  199. data/lib/slices/i18n/backend.rb +32 -0
  200. data/lib/slices/i18n_backend.rb +24 -0
  201. data/lib/slices/paperclip.rb +13 -0
  202. data/lib/slices/position_helper.rb +98 -0
  203. data/lib/slices/renderer.rb +52 -0
  204. data/lib/slices/slices_engine.rb +51 -0
  205. data/lib/slices/split_date_time_field.rb +14 -0
  206. data/lib/slices/tasks/assets.rake +35 -0
  207. data/lib/slices/tasks/db.rake +50 -0
  208. data/lib/slices/tasks/seeds.rake +93 -0
  209. data/lib/slices/tasks/validate.rake +62 -0
  210. data/lib/slices/tree.rb +306 -0
  211. data/lib/slices/version.rb +4 -0
  212. data/lib/slices/will_paginate.rb +12 -0
  213. data/lib/slices/will_paginate_mongoid.rb +45 -0
  214. data/lib/standard_tree.rb +193 -0
  215. metadata +483 -0
@@ -0,0 +1,191 @@
1
+ // Responsible for an invidiual thumbnail. This view is intended to remain
2
+ // fairly dumb - let AssetLibraryView handle events.
3
+ slices.AssetThumbView = Backbone.View.extend({
4
+
5
+ tagName: 'li',
6
+ className: 'asset-library-item',
7
+
8
+ happyTime: 1000,
9
+
10
+ template: Handlebars.compile(
11
+ '<img src="{{src}}" alt="{{name}}">' +
12
+ '<span class="name">{{displayName}}</span>' +
13
+ '<dl class="meta">' +
14
+ '<dd><strong class="filename">{{name}}</strong></dd>' +
15
+ '<dd>{{size}}</dd>' +
16
+ '<dd>Added {{createdAt}}</dd>' +
17
+ '<dd><a href="{{url}}" data-action="edit">Edit</a></dd>' +
18
+ '</dl>'
19
+ ),
20
+
21
+ events: {
22
+ 'mousedown' : 'press',
23
+ 'mouseup' : 'release',
24
+ 'mousedown [data-action="edit"]' : 'pressEdit',
25
+ 'click [data-action="edit"]' : 'releaseEdit'
26
+ },
27
+
28
+ initialize: function() {
29
+ _.bindAll(this);
30
+ this.$el.append('<div class="asset-details">');
31
+ this.model.bind('change', this.whenModelChanges);
32
+ this.model.bind('destroying', this.whenModelIsDestroying);
33
+ },
34
+
35
+ render: function() {
36
+ this.$el.find('.asset-details').html(this.template(this));
37
+ return this;
38
+ },
39
+
40
+ src: function() {
41
+ return this.model.get('asset_url')
42
+ || '<%= asset_path 'slices/icon_generic_file.png' %>';
43
+ },
44
+
45
+ name: function() {
46
+ return this.model.get('name');
47
+ },
48
+
49
+ url: function() {
50
+ return this.model.url();
51
+ },
52
+
53
+ displayName: function() {
54
+ if (!this.model.isImage()) return this.name();
55
+ },
56
+
57
+ createdAt: function() {
58
+ return moment(this.model.get('created_at')).calendar();
59
+ },
60
+
61
+ size: function() {
62
+ return humanFileSize(this.model.get('file_file_size'));
63
+ },
64
+
65
+ select: function() {
66
+ $(this.el).addClass('selected');
67
+ },
68
+
69
+ deselect: function() {
70
+ $(this.el).removeClass('selected');
71
+ },
72
+
73
+ selected: function() {
74
+ return $(this.el).hasClass('selected');
75
+ },
76
+
77
+ remove: function() {
78
+ this.unbind();
79
+ $(this.el).remove();
80
+ },
81
+
82
+ press: function(event) {
83
+ if (!this.options.selectable) return;
84
+ if (event.which !== 1) return;
85
+ event.preventDefault();
86
+ event.stopImmediatePropagation();
87
+ this.trigger('thumb:press', event, this);
88
+ },
89
+
90
+ release: function(event) {
91
+ if (!this.options.selectable) return;
92
+ if (event.which !== 1) return;
93
+ this.trigger('thumb:release', event, this);
94
+ },
95
+
96
+ pressEdit: function(e) {
97
+ e.stopImmediatePropagation();
98
+ },
99
+
100
+ releaseEdit: function(e) {
101
+ e.preventDefault();
102
+ e.stopImmediatePropagation();
103
+
104
+ this.trigger('thumb:blur');
105
+
106
+ var el = $(this.el);
107
+ el.addClass('editing');
108
+
109
+ var editor = slices.AssetEditorView.openModal({ model: this.model });
110
+ editor.bind('close', function() {
111
+ _.delay(function() { el.removeClass('editing') }, 350);
112
+ });
113
+ },
114
+
115
+ whenModelChanges: function() {
116
+ this.render();
117
+ },
118
+
119
+ whenModelIsDestroying: function() {
120
+ $(this.el).addClass('destroying');
121
+ },
122
+
123
+ // Update the upload progress information, wrapped around the file.
124
+ updateFile: function(attrs) {
125
+ if (!this.fileView) this.makeFileView();
126
+
127
+ $(this.el).
128
+ removeClass(this.fileView.possibleStatusList).
129
+ addClass('status-' + this.fileView.model.status());
130
+ },
131
+
132
+ // Update the upload progress information so we see happy face, then
133
+ // wait for the thumbnail to load or happyTime, whichever is longer.
134
+ updateFileAndComplete: function(file) {
135
+ this.updateFile(file);
136
+ this.gracefullyRemoveFileView();
137
+ },
138
+
139
+ // Make fileview, this gets done on the fly.
140
+ makeFileView: function() {
141
+ this.fileView = new slices.FileView({
142
+ model: this.model.get('file')
143
+ });
144
+ this.$el.append(this.fileView.el);
145
+ $(this.fileView.el).css({ position: 'absolute', top: 0 });
146
+ },
147
+
148
+ // Remove fileview
149
+ removeFileView: function() {
150
+ if (this.fileView) {
151
+ this.fileView.remove();
152
+ delete this.fileView;
153
+ }
154
+ },
155
+
156
+ // Wait for thumnail to load and happyTime to pass, then complete the
157
+ // transition to showing our lovely new thumbnail.
158
+ gracefullyRemoveFileView: function() {
159
+ $.when(
160
+ this.thumbnailHasLoaded(),
161
+ this.happyTimeHasPassed()
162
+ ).then(this.resolveGracefully);
163
+ },
164
+
165
+ // Complete transition by fading out fileView, rendering, then
166
+ // fading our thumbnail in.
167
+ resolveGracefully: function() {
168
+ this.$('.asset-details').css({ opacity: 0 });
169
+
170
+ $(this.fileView.el).animate({ opacity: 0 }, 'fast', _.bind(function() {
171
+ this.removeFileView();
172
+ this.$('.asset-details').animate({ opacity: 1 });
173
+ }, this));
174
+ },
175
+
176
+ // Returns a deferred promise wrapping thumbnail pre-load.
177
+ thumbnailHasLoaded: function() {
178
+ var dfd = new $.Deferred();
179
+ this.$('img').load(dfd.resolve);
180
+ return dfd.promise();
181
+ },
182
+
183
+ // Returns a deferred promise wrapping happyTime.
184
+ happyTimeHasPassed: function() {
185
+ var dfd = new $.Deferred();
186
+ _.delay(dfd.resolve, this.happyTime);
187
+ return dfd.promise();
188
+ }
189
+
190
+ });
191
+
@@ -0,0 +1,350 @@
1
+ // Responsible for managing the ui for a collection of attachments. Works in
2
+ // conjunction with `AttachmentView`, `Attachment`, `AttachmentCollection`
3
+ // and specialized Handlebars helper `attachmentComposer`.
4
+ // A JSON description of the collection is written to the element’s
5
+ // data-computed-value, and subsequently read by Slices when saving the Page.
6
+ //
7
+ // This shouldn’t be instantiated directly.
8
+ // Instead, use `{{attachmentComposer}}` like this:
9
+ //
10
+ // {{#attachmentComposer myAttachments}}
11
+ // <textarea name="caption">{{caption}}</textarea>
12
+ // {{/attachmentComposer}}
13
+ //
14
+ slices.AttachmentComposerView = Backbone.View.extend({
15
+
16
+ DROP_THRESHOLD: 15,
17
+
18
+ views: {}, // internal view cache
19
+
20
+ events: {
21
+ 'click [data-action="library"]' : 'openAssetDrawer',
22
+ 'click [data-action="remove"]' : 'removeClicked'
23
+ },
24
+
25
+ template: Handlebars.compile(
26
+ '<ol class="attachment-list"></ol>' +
27
+ '<div class="attachment-actions">' +
28
+ '<button data-action="library">Choose from Library</button>' +
29
+ '<button data-action="upload">Upload from computer</button>' +
30
+ '</div>'
31
+ ),
32
+
33
+ className: 'attachment-composer',
34
+
35
+ broadcastChanges: true,
36
+
37
+ // Initialize the view. There are a few steps here, so read on.
38
+ initialize: function() {
39
+ _.bindAll(this);
40
+
41
+ // If this.options.collection is just a simple array, we need to
42
+ // instantiate and AttachmentCollection.
43
+ this.collection = new slices.AttachmentCollection(this.options.collection);
44
+ this.collection.bind('add' , this.addAttachment);
45
+ this.collection.bind('remove' , this.removeAttachment);
46
+ this.collection.bind('change' , this.update);
47
+ this.collection.bind('reset' , this.update);
48
+
49
+ // Listen out for asset drags and drops.
50
+ $(window).on('assets:dragStarted', this.onAssetDragStarted);
51
+
52
+ if (this.options.autoAttach) {
53
+ // Defer the attachment of the real view element.
54
+ _.defer(this.attach);
55
+ }
56
+ },
57
+
58
+ // Placeholder element to render into later.
59
+ placeholder: function() {
60
+ return Handlebars.compile('<div id="placeholder-{{id}}"></div>')(this);
61
+ },
62
+
63
+ render: function() {
64
+ this.broadcastChanges = false;
65
+ $(this.el).html(this.template(this));
66
+ this.collection.each(this.addAttachment);
67
+ this.makeSortable();
68
+ this.makeUploader();
69
+ this.update();
70
+ this.broadcastChanges = true;
71
+ return this;
72
+ },
73
+
74
+ // Replace our placeholder element with this.el.
75
+ attach: function() {
76
+ $('#placeholder-' + this.id).replaceWith(this.el);
77
+ this.render();
78
+ },
79
+
80
+ // Add ui and references for an attachment.
81
+ addAttachment: function(attachment, collection, options) {
82
+ var view = new slices.AttachmentView({
83
+ fields: this.options.fields,
84
+ model: attachment
85
+ });
86
+
87
+ if (options.index < collection.length - 1) {
88
+ view.$el.insertBefore(this.$('.attachment-list').children()[options.index]);
89
+ } else {
90
+ this.$('.attachment-list').append(view.el);
91
+ }
92
+
93
+ view.render();
94
+ this.views[attachment.cid] = view;
95
+ this.update();
96
+ },
97
+
98
+ // Remove ui and references for an attachment.
99
+ removeAttachment: function(attachment) {
100
+ var view = this.views[attachment.cid];
101
+ view.remove();
102
+ delete this.views[attachment.cid];
103
+ if (attachment.file) this.uploader.removeFile(attachment.file);
104
+ this.update();
105
+ },
106
+
107
+ // Write a JSON representation of the collection into data-computed-value
108
+ // on this.el. Slices picks up on the computed-value when saving the page.
109
+ // Ignores any items with a null asset_id, which are likely to be
110
+ // failed uploads.
111
+ update: function() {
112
+ var value = this.collection.toJSON(),
113
+ $el = $(this.el);
114
+
115
+ value = _.reject(value, function(a) { return a.asset_id == null });
116
+ $el.data('computed-value', value);
117
+ $el[this.collection.isEmpty() ? 'removeClass' : 'addClass']('not-empty');
118
+ if (this.broadcastChanges) $el.trigger('change');
119
+ },
120
+
121
+ // Infers a view and asset from just-clicked button and attempts to remove
122
+ // the asset from the collection. Will prevent action if upload is in
123
+ // progress - not ideal, but Plupload doesn't support cancelling an
124
+ // in-progress uploader.
125
+ removeClicked: function(e) {
126
+ var button = $(e.target),
127
+ view = button.parent('li'),
128
+ attachment = view.data('model');
129
+
130
+ this.collection.remove(attachment);
131
+ },
132
+
133
+ // Shows the asset library.
134
+ openAssetDrawer: function(e) {
135
+ e.preventDefault();
136
+ e.stopImmediatePropagation();
137
+ slices.assetDrawer().open({ step: this.assetDrawerStep });
138
+ },
139
+
140
+ assetDrawerStep: function() {
141
+ var el = $(this.el),
142
+ bottom = el.offset().top + el.outerHeight() + 30,
143
+ drawerTop = $(slices.assetDrawer().el).offset().top;
144
+
145
+ if (bottom > drawerTop) {
146
+ var body = $('body');
147
+ body.scrollTop(body.scrollTop() + (bottom - drawerTop));
148
+ }
149
+ },
150
+
151
+ // Returns the Attachment view responsible for given File.
152
+ viewForFile: function(file) {
153
+ return this.views[file.attachment.cid];
154
+ },
155
+
156
+ // Make items sortable using jQuery UI sortable plugin.
157
+ makeSortable: function() {
158
+ this.$('.attachment-list').sortable({
159
+ handle: '.attachment-thumb',
160
+ scroll: false,
161
+
162
+ beforeStart: _.bind(function(e, ui) {
163
+ this.attachmentList().freezeHeight();
164
+ window.autoscroll.start();
165
+ }, this),
166
+
167
+ stop: _.bind(function(e, ui) {
168
+ this.attachmentList().thawHeight();
169
+ window.autoscroll.stop();
170
+ }, this),
171
+
172
+ update: this.updateOnSort
173
+ });
174
+ },
175
+
176
+ // Update collection to match the visible order of item elements.
177
+ // We avoid jQuery.map here, because it returns some sort of weird
178
+ // jquery object, rather than an array.
179
+ updateOnSort: function() {
180
+ var newOrder = _.map(this.$('.attachment-list li').get(), function(li) {
181
+ return $(li).data('model');
182
+ });
183
+ this.collection.reset(newOrder);
184
+ },
185
+
186
+ // Create an uploader instance and bind up its callbacks.
187
+ makeUploader: function() {
188
+ this.uploader = new slices.Uploader({
189
+ button : this.$('[data-action="upload"]'),
190
+ drop : this.el
191
+ });
192
+ this.uploader.bind('filesAdded', this.onFilesAdded);
193
+ this.uploader.bind('fileUploaded', this.onFileUploaded);
194
+ },
195
+
196
+ // When files are added to the upload queue we create corresponding
197
+ // attachment objects and add them to the collection.
198
+ onFilesAdded: function(event) {
199
+ var files = event.files,
200
+ uploader = event.uploader;
201
+
202
+ _(files).each(function(file) {
203
+ var a = new slices.Attachment({
204
+ asset: new slices.Asset({ file: file })
205
+ });
206
+ // This is clearly a code-smell, but it lets us easily look-up
207
+ // the attachment-view for this file when events occur.
208
+ file.attachment = a;
209
+ // These bits are fine.
210
+ this.collection.add(a);
211
+ this.updateFileStatus(file);
212
+ }, this);
213
+
214
+ this.uploader.start();
215
+ },
216
+
217
+ // This looks weird, I know, but really all we’re doing is taking the
218
+ // response from our upload to /assets and feeding it into our
219
+ // attachment model.
220
+ onFileUploaded: function(event) {
221
+ var file = event.file,
222
+ response = event.response,
223
+ attachment = file.attachment,
224
+ asset = attachment.get('asset');
225
+
226
+ // Update the attachment model with just the new asset_id and update
227
+ // the underlying asset with all the new info. This could do with
228
+ // refactoring to make it better reveal its intent.
229
+ attachment.set({ asset_id: response.id });
230
+ asset.set(response);
231
+ // Finally complete upload progress display and transition to thumbnail.
232
+ this.viewForFile(file).updateFileAndComplete(file);
233
+ // Need to update when uploads complete, so data('value') correctly
234
+ // reflects all these attachments.
235
+ this.update();
236
+ // Send the signal to any asset library views.
237
+ $(window).trigger('assets:uploadCompleted');
238
+ },
239
+
240
+ // Tell the appropriate attachment object to update against the file.
241
+ // This needs to be deferred, for reasons mentioned above.
242
+ updateFileStatus: function(file) {
243
+ this.viewForFile(file).updateFile(file);
244
+ },
245
+
246
+ // The following methods implement the asset-receiver interface.
247
+ // See slices.AssetLibraryView for details.
248
+
249
+ onAssetDragStarted: function(e, library) {
250
+ library.registerReceiver(this);
251
+ },
252
+
253
+ withinBounds: function(x, y) {
254
+ var offset = $(this.el).offset(),
255
+ top = offset.top - this.DROP_THRESHOLD,
256
+ left = offset.left - this.DROP_THRESHOLD,
257
+ bottom = top + $(this.el).height() + (this.DROP_THRESHOLD * 2),
258
+ right = left + $(this.el).width() + (this.DROP_THRESHOLD * 2);
259
+
260
+ return x >= left && x <= right && y >= top && y <= bottom;
261
+ },
262
+
263
+ cursor: function() {
264
+ return this._cursor = this._cursor || $('<li class="cursor">');
265
+ },
266
+
267
+ assetsOver: function(x, y) {
268
+ this.$el.addClass('assets-over');
269
+
270
+ // So as not to cause excessive re-flows, we only want to move the cursor
271
+ // when the placement point actually changes.
272
+ //
273
+ // The following steps aren’t pretty, and could do with a refactor.
274
+ //
275
+ // Find the new placement point.
276
+ var p = this.findPlacementPoint(x, y);
277
+ // If we’ve a placement point in memory, and a new point was found,
278
+ // and they share the same index, then we won’t be moving.
279
+ var same = (this.placementPoint && p && this.placementPoint.index === p.index);
280
+ // Commit the placement point to memory.
281
+ this.placementPoint = p;
282
+ // If the point hasn’t changed, duck out at this point.
283
+ if (same) return;
284
+
285
+ // If a placement point was found, insert the cursor before.
286
+ if (p) {
287
+ this.cursor().insertBefore(p.view.el);
288
+ // Otherwise, just append to attachment-list.
289
+ } else {
290
+ this.cursor().appendTo(this.$('.attachment-list'));
291
+ }
292
+ },
293
+
294
+ assetsNotOver: function() {
295
+ this.$el.removeClass('assets-over');
296
+ this.cursor().detach();
297
+ delete this.placementPoint;
298
+ },
299
+
300
+ receiveAssets: function(assets, x, y) {
301
+ var p = this.findPlacementPoint(x, y),
302
+ position = p ? p.index : this.collection.length;
303
+
304
+ _.each(assets, function(asset) {
305
+ if (this.alreadyContains(asset)) return;
306
+ this.collection.add({ asset: asset, asset_id: asset.get('id') }, { at: position });
307
+ position += 1;
308
+ }, this);
309
+ },
310
+
311
+ // Returns the point at which an asset drop should be placed for the
312
+ // given coordinates, as a Hash containing the following model, following
313
+ // view and exact index for placement. If no suitable point is found, it
314
+ // returns null.
315
+ //
316
+ // findPlacementPoint(0, 0) //-> { attachment: a, view: v, :index: i }
317
+ //
318
+ findPlacementPoint: function(x, y) {
319
+ var result = null,
320
+ views = this.views;
321
+
322
+ this.collection.find(function(a, i) {
323
+ var v = views[a.cid];
324
+
325
+ if (v.midPoint().y > y) {
326
+ result = { attachment: a, view: v, index: i };
327
+ return true;
328
+ }
329
+
330
+ return false;
331
+ });
332
+
333
+ return result;
334
+ },
335
+
336
+ // Returns true if the given asset is already in our collection.
337
+ alreadyContains: function(asset) {
338
+ var id = asset.get('id');
339
+
340
+ return this.collection.any(function(attachment) {
341
+ return attachment.get('asset_id') === id;
342
+ });
343
+ },
344
+
345
+ attachmentList: function() {
346
+ return this.$('.attachment-list');
347
+ }
348
+
349
+ });
350
+