slices 1.0.0

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 (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
+