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,209 @@
1
+ // This view presents all useful details about an asset, and allows for
2
+ // actions such as replacement and deletion.
3
+ slices.AssetEditorView = Backbone.View.extend({
4
+
5
+ className: 'asset-editor',
6
+
7
+ // This template is a monster and needs to be extracted. We could aim to
8
+ // follow the Backbone/Jammit pattern, but aiming for asset pipeline
9
+ // integration seems a more worthwhile end goal.
10
+ template: Handlebars.compile(
11
+ '<div class="fields">' +
12
+ '<div class="overview">' +
13
+ '<div class="thumb asset-library-item">' +
14
+ '<img src="{{thumbUrl}}" alt="{{name}}">' +
15
+ '<span class="name">{{displayName}}</span>' +
16
+ '</div>' +
17
+ '<div class="meta">' +
18
+ '<dl class="details">' +
19
+ '<dd><input type="text" name="name" value="{{name}}"></dd>' +
20
+ '<dd>{{size}}</dd>' +
21
+ '<dd>Added {{createdAt}}</dd>' +
22
+ '<dd><a href="{{originalUrl}}">Download original</a></dd>' +
23
+ '</dl>' +
24
+ '<div class="options">' +
25
+ '<button data-action="replace">Upload a New Version</button>' +
26
+ '</div>' +
27
+ '</div>' +
28
+ '</div>' +
29
+ '<div class="tabs">' +
30
+ '<div class="tab meta">' +
31
+ '<label>Tags</label>' +
32
+ '<textarea name="tags" rows="5">{{tags}}</textarea>' +
33
+ '</div>' +
34
+ '{{#if pages}}' +
35
+ '<div class="tab pages">' +
36
+ '<label>Appears on</label>' +
37
+ '<ul>' +
38
+ '{{{pages}}}' +
39
+ '</ul>' +
40
+ '</div>' +
41
+ '{{/if}}' +
42
+ '</div>' +
43
+ '</div>' +
44
+ '<div class="bottom-toolbar">' +
45
+ '<div class="button-group aligned-right">' +
46
+ '<button type="submit" data-action="save">Save Changes</button>' +
47
+ '<button data-action="cancel">Cancel</button>' +
48
+ '</div>' +
49
+ '<div class="button-group pinned left top">' +
50
+ '<button data-action="delete" class="delete">Delete</button>' +
51
+ '</div>' +
52
+ '</div>'
53
+ ),
54
+
55
+ pageItemTemplate: Handlebars.compile(
56
+ '<li><a href="/admin/pages/{{id}}" target="_blank">{{name}}</a></li>'
57
+ ),
58
+
59
+ events: {
60
+ 'click [data-action="save"]' : 'save',
61
+ 'click [data-action="cancel"]' : 'cancel',
62
+ 'click [data-action="delete"]' : 'destroy'
63
+ },
64
+
65
+ initialize: function() {
66
+ _.bindAll(this);
67
+ this.model.on('change', this.render);
68
+ },
69
+
70
+ render: function() {
71
+ $(this.el).html(this.template(this));
72
+ this.makeUploader();
73
+ return this;
74
+ },
75
+
76
+ thumbUrl: function() {
77
+ return this.model.get('asset_url')
78
+ || '<%= asset_path 'slices/icon_generic_file.png' %>';
79
+ },
80
+
81
+ originalUrl: function() {
82
+ return this.model.get('original_url');
83
+ },
84
+
85
+ name: function() {
86
+ return this.model.get('name');
87
+ },
88
+
89
+ tags: function() {
90
+ return this.model.get('tags');
91
+ },
92
+
93
+ // Handlebars does not call functions passed to `#each` and `#if`,
94
+ // so `pages` has to render a collections of sub-templates.
95
+ pages: function() {
96
+ return _.map(this.model.get('pages'), this.pageItemTemplate).join('');
97
+ },
98
+
99
+ url: function() {
100
+ return this.model.url();
101
+ },
102
+
103
+ displayName: function() {
104
+ if (!this.model.isImage()) return this.name();
105
+ },
106
+
107
+ createdAt: function() {
108
+ return moment(this.model.get('created_at')).calendar();
109
+ },
110
+
111
+ size: function() {
112
+ return humanFileSize(this.model.get('file_file_size'));
113
+ },
114
+
115
+ // At present, it’s beneficial to specify exactly which attributes we’re
116
+ // sending back to Slices. This may be automated in future, but for now,
117
+ // remember to add any other fields in here.
118
+ save: function(e) {
119
+ this.model.save({
120
+ name: this.$('[name="name"]').val(),
121
+ tags: this.$('[name="tags"]').val()
122
+ }, {
123
+ success: this.whenSaveSucceeds,
124
+ error: this.whenSaveFails
125
+ });
126
+ },
127
+
128
+ whenSaveSucceeds: function(model, response) {
129
+ this.close();
130
+ },
131
+
132
+ whenSaveFails: function(model, response) {},
133
+
134
+ cancel: function(e) {
135
+ this.close();
136
+ },
137
+
138
+ destroy: function(e) {
139
+ if (confirm('Are you sure you want to delete this asset?')) {
140
+ this.model.destroy();
141
+ this.close();
142
+ }
143
+ },
144
+
145
+ close: function() {
146
+ this.trigger('close');
147
+ },
148
+
149
+ makeUploader: function() {
150
+ this.uploader = new slices.Uploader({
151
+ button : this.$('[data-action="replace"]'),
152
+ drop : this.$('.thumb'),
153
+ url : this.model.url(),
154
+ params : { _method: 'put' }
155
+ });
156
+ this.uploader.bind('filesAdded', this.onFilesAdded);
157
+ this.uploader.bind('fileUploaded', this.onFileUploaded);
158
+ },
159
+
160
+ onFilesAdded: function(event) {
161
+ var files = event.files,
162
+ file = files[0];
163
+
164
+ this.fileView = new slices.FileView({ model: file });
165
+ this.$('.thumb').append(this.fileView.el);
166
+ this.fileView.model.upload(file);
167
+
168
+ this.uploader.start();
169
+ },
170
+
171
+ onFileUploaded: function(event) {
172
+ this.model.set(event.response);
173
+ },
174
+
175
+ updateFileStatus: function(file) {
176
+ this.fileView.model.upload(file);
177
+ },
178
+
179
+ });
180
+
181
+ // This somewhat ungainly method opens the editor in a modal.
182
+ slices.AssetEditorView.openModal = function(options) {
183
+ var view = new slices.AssetEditorView(options);
184
+ var modal = $('<div class="jqmWindow modal" id="asset_editor_modal">');
185
+
186
+ modal.
187
+ appendTo('body').
188
+ append(view.el).
189
+ jqm({
190
+ modal: true,
191
+ onShow: function (h) {
192
+ h.w.fadeIn(150);
193
+ modal.fadeIn(300);
194
+ },
195
+ onHide: function (h) {
196
+ h.w.fadeOut(150, function() { view.remove() });
197
+ h.o.fadeOut(300, function() { modal.remove() });
198
+ },
199
+ overlay: 40
200
+ }).
201
+ jqmShow();
202
+
203
+ view.render();
204
+
205
+ view.bind('close', function() { modal.jqmHide() });
206
+
207
+ return view;
208
+ }
209
+
@@ -0,0 +1,720 @@
1
+ // The asset library is dedicated to browsing, searching and managing of
2
+ // assets. It can be rendered in-place, or to a specific element using the
3
+ // helper method.
4
+ //
5
+ // To render the library inline in html:
6
+ //
7
+ // <script>slices.renderAssetLibrary()</script>
8
+ //
9
+ // Otherwise:
10
+ //
11
+ // slices.renderAssetLibrary({ el: '#my-target' })
12
+ //
13
+ slices.AssetLibraryView = Backbone.View.extend({
14
+
15
+ DRAG_THRESHOLD: 15,
16
+
17
+ DRAWER_HEIGHT: 250,
18
+
19
+ events: {
20
+ 'keyup [type="search"]' : 'search',
21
+ 'click [type="search"]' : 'search',
22
+ 'click [data-action="close"]' : 'close',
23
+ 'click [data-action="show-all"]' : 'showAll',
24
+ 'mousedown .library-container' : 'backgroundPress',
25
+ 'mousedown .resize-handle' : 'startResize'
26
+ },
27
+
28
+ thumbs: {},
29
+
30
+ selection: [],
31
+
32
+ currentSearchTerm: '',
33
+
34
+ className: 'asset-library',
35
+
36
+ // We include the full template here, so the view can be rendered anywhere
37
+ // without html needing to be present ahead of time.
38
+ template: Handlebars.compile(
39
+ '<div class="toolbar">' +
40
+ '{{{resizeHandle}}}' +
41
+ '<input type="search" id="assets-search" placeholder="Search assets...">' +
42
+ '<div class="count"></div>' +
43
+ '{{{hint}}}' +
44
+ '<div class="actions"><ul>{{actions}}</ul></div>' +
45
+ '</div>' +
46
+ '<div class="library-container">' +
47
+ '<ul class="library"></ul>' +
48
+ '</div>'
49
+ ),
50
+
51
+ // The collection is instantiated during initialization, as this is an
52
+ // 'AppView' style class.
53
+ initialize: function() {
54
+ _.bindAll(this);
55
+
56
+ this.collection = new slices.AssetCollection();
57
+ this.collection.bind('add', this.add);
58
+ this.collection.bind('remove', this.remove);
59
+ this.collection.bind('reset', this.reset);
60
+ $(window).on('assets:uploadCompleted', this.fetch);
61
+
62
+ this.render();
63
+
64
+ this.scrollArea = this.$('.library-container');
65
+ this.scrollArea.on('scroll', this.onScroll);
66
+
67
+ this.initializeUploader();
68
+
69
+ this.showLoadingSpinner();
70
+
71
+ this.fetch();
72
+ },
73
+
74
+ render: function() {
75
+ $(this.el).html(this.template(this));
76
+ return this;
77
+ },
78
+
79
+ // Clear all thumbs on display and re-render the collection.
80
+ reset: function() {
81
+ this.clearThumbs();
82
+ this.collection.each(this.add);
83
+ },
84
+
85
+ // Create a thumb view for the model, render it, and stick it in
86
+ // our reference hash.
87
+ add: function(model) {
88
+ var thumb = new slices.AssetThumbView({ model: model, selectable: true });
89
+ thumb.bind('thumb:press', this.thumbPress);
90
+ thumb.bind('thumb:release', this.thumbRelease);
91
+ thumb.bind('thumb:blur', this.thumbBlur);
92
+ this.loadingSpinner().before(thumb.render().el);
93
+ this.thumbs[model.cid] = thumb;
94
+ this.updateCount();
95
+ },
96
+
97
+ // Ask the thumb view to remove itself, and delete the reference.
98
+ remove: function(model) {
99
+ this.clearThumb(model.cid);
100
+ this.updateCount();
101
+ },
102
+
103
+ // Handle special keystrokes, or defer to our debounced general search.
104
+ search: function(e) {
105
+ if (e.which == 27) this.$('[type="search"]').val('');
106
+ this._search(e);
107
+ },
108
+
109
+ // This debounced method actually performs the search on our collection.
110
+ _search: _.debounce(function(e) {
111
+ var term = this.searchTerm();
112
+
113
+ if (term === this.currentSearchTerm) return;
114
+
115
+ this.currentSearchTerm = term;
116
+ this.fetch();
117
+ }, 300),
118
+
119
+ // Assuming we’re in upload-view, take us back to normal.
120
+ showAll: function() {
121
+ if (this.uploader.files.length > 0) {
122
+ alert('Uploads are still in progress, please wait for them to finish.');
123
+ } else {
124
+ this.fetch();
125
+ }
126
+ },
127
+
128
+ // Remove all thumb views currently on display.
129
+ clearThumbs: function() {
130
+ this.updateCount('Searching…');
131
+ for (var cid in this.thumbs) this.clearThumb(cid);
132
+ },
133
+
134
+ // Remove a specific thumb.
135
+ clearThumb: function(cid) {
136
+ var thumb = this.thumbs[cid];
137
+ this.selection = _(this.selection).without(thumb);
138
+ thumb.remove();
139
+ delete this.thumbs[cid];
140
+ },
141
+
142
+ // Helper method for getting the value of the search field.
143
+ searchTerm: function() {
144
+ return this.$('[type="search"]').val();
145
+ },
146
+
147
+ // If we’re in drawer mode, we’ll display a hint that users should
148
+ // drag and drop.
149
+ hint: function() {
150
+ if (this.options.mode === 'drawer') {
151
+ return '<div class="hint">Drag &amp; drop to place on the page</div>';
152
+ }
153
+ },
154
+
155
+ // If we’re in drawer mode, we’ll add a resize handle in here.
156
+ resizeHandle: function() {
157
+ if (this.options.mode === 'drawer') {
158
+ return '<div class="resize-handle"></div>';
159
+ }
160
+ },
161
+
162
+ // We render different action button depending on the mode.
163
+ actions: function() {
164
+ var actions = [];
165
+
166
+ actions.push('<li><a class="button" data-action="upload">Upload</a></li>');
167
+
168
+ if (this.options.mode === 'drawer') {
169
+ actions.push('<li><a class="button" data-action="close">Close</a></li>');
170
+ }
171
+
172
+ return new Handlebars.SafeString(actions.join(''));
173
+ },
174
+
175
+ // The open and close methods only apply if in 'drawer' mode.
176
+ open: function(options) {
177
+ $('#container').stop().animate({ paddingBottom: this.DRAWER_HEIGHT + 'px' });
178
+ $(this.el).stop().animate(
179
+ { height: this.DRAWER_HEIGHT + 'px' },
180
+ _.extend({}, options, { complete: this.afterOpen })
181
+ );
182
+ },
183
+ afterOpen: function() {
184
+ this.$('input[type="search"]')[0].focus();
185
+ },
186
+ close: function() {
187
+ this.$('input[type="search"]')[0].blur();
188
+ $('#container').stop().animate({ paddingBottom: '0px' });
189
+ $(this.el).stop().animate({ height: '0px' });
190
+ },
191
+
192
+ // Thumb press/release behaviour mimicks that of operating system file
193
+ // selection. Holding down shift selects ranges of files, with memory of
194
+ // the anchor. Holding down cmd/ctrl selects multiple assets. Clicks
195
+ // without modifiers select single assets.
196
+ //
197
+ // This method is disgraceful at the moment, because there are a few
198
+ // modes of behaviour. It’s an ideal candidate for refactoring.
199
+ thumbPress: function(event, thumb) {
200
+ _.invoke(this.selection, 'deselect');
201
+
202
+ if (event.shiftKey) {
203
+ if (this.selection.length === 0) {
204
+ this.selection.push(thumb);
205
+ this.selectionAnchor = thumb;
206
+ } else {
207
+ var first = this.selectionAnchor,
208
+ start = this.collection.indexOf(first.model),
209
+ end = this.collection.indexOf(thumb.model),
210
+ lowest = Math.min(start, end),
211
+ highest = Math.max(start, end);
212
+
213
+ this.selection = [];
214
+
215
+ for (var i = lowest; i <= highest; i++) {
216
+ var thumb = this.thumbs[this.collection.at(i).cid];
217
+ this.selection.push(thumb);
218
+ }
219
+ }
220
+ } else if (event.metaKey) {
221
+ if (_(this.selection).include(thumb)) {
222
+ this.selection = _(this.selection).without(thumb);
223
+ } else {
224
+ this.selection.push(thumb);
225
+ }
226
+ this.selectionAnchor = _.sortBy(this.selection, function(thumb) {
227
+ return thumb.model.collection.indexOf(thumb.model);
228
+ })[0];
229
+ } else {
230
+ if (!_(this.selection).include(thumb)) {
231
+ this.selection = [thumb];
232
+ this.selectionAnchor = thumb;
233
+ }
234
+ }
235
+
236
+ _.invoke(this.selection, 'select');
237
+
238
+ if (this.selection.length > 0) this.prepareForDrag(event);
239
+
240
+ this.listenForKeys();
241
+ },
242
+
243
+ // Most selection releated behaviours happen on mousedown. The exception
244
+ // is when multiple assets are selected - a single unmodified click will
245
+ // set the selection to the clicked asset, but only when the mouse is
246
+ // released.
247
+ thumbRelease: function(event, thumb) {
248
+ if (event.metaKey || event.shiftKey) return;
249
+
250
+ _.invoke(this.selection, 'deselect');
251
+ this.selection = [thumb];
252
+ this.selectionAnchor = thumb;
253
+ _.invoke(this.selection, 'select');
254
+ },
255
+
256
+ // Thumb Blur is not the best name, but this basically implies that an
257
+ // action has taken place which should cause asset library thumb-related
258
+ // actions to take a back-seat. This is used when the asset editor is
259
+ // opened.
260
+ thumbBlur: function() {
261
+ this.deselectAll();
262
+ },
263
+
264
+ // When the background is pressed, the selection is cleared out.
265
+ backgroundPress: function(event) {
266
+ if (event.shiftKey || event.metaKey) return;
267
+ event.preventDefault();
268
+ event.stopImmediatePropagation();
269
+ this.deselectAll();
270
+ },
271
+
272
+ // Deselect all thumbs and trigger appropriate actions.
273
+ deselectAll: function() {
274
+ _.invoke(this.selection, 'deselect');
275
+ this.selection = [];
276
+ },
277
+
278
+ // Array of models from the current selection.
279
+ selectedAssets: function() {
280
+ return _.pluck(this.selection, 'model');
281
+ },
282
+
283
+ // Update the UI count of asset(s) found.
284
+ updateCount: function(message) {
285
+ this.$('.count').html(message || this.countMessage());
286
+ },
287
+
288
+ // Messages for different asset counts.
289
+ countMessages: {
290
+ default: {
291
+ none: Handlebars.compile('No assets, yet…'),
292
+ one: Handlebars.compile('Showing 1 asset'),
293
+ some: Handlebars.compile(
294
+ 'Showing latest {{count}} assets of {{total}} total'
295
+ ),
296
+ all: Handlebars.compile(
297
+ 'Showing all {{count}} assets, latest first'
298
+ )
299
+ },
300
+ uploads: {
301
+ one: Handlebars.compile(
302
+ 'Showing 1 new asset ' +
303
+ '(<a data-action="show-all">Show everything</a>)'
304
+ ),
305
+ some: Handlebars.compile(
306
+ 'Showing {{count}} new assets ' +
307
+ '(<a data-action="show-all">Show everything</a>)'
308
+ ),
309
+ },
310
+ search: {
311
+ none: Handlebars.compile('No matching assets'),
312
+ one: Handlebars.compile('Showing 1 matching asset'),
313
+ some: Handlebars.compile(
314
+ 'Showing latest {{count}} matching assets of {{total}} total'
315
+ ),
316
+ all: Handlebars.compile(
317
+ 'Showing all {{count}} maching assets, latest first'
318
+ )
319
+ }
320
+ },
321
+
322
+ // Returns the appropriate message count for the current context.
323
+ countMessage: function() {
324
+ var t = this.countMessages,
325
+ total = this.collection.totalEntries,
326
+ count = this.collection.length;
327
+
328
+ if (this.inUploadView) {
329
+ t = t.uploads;
330
+
331
+ if (count === 1) {
332
+ return t.one({ count: count });
333
+ } else {
334
+ return t.some({ count: count });
335
+ }
336
+ } else {
337
+ t = this.currentSearchTerm ? t.search : t.default;
338
+
339
+ if (total === 0) {
340
+ return t.none();
341
+ } else if (total === 1) {
342
+ return t.one();
343
+ } else if (total > count) {
344
+ return t.some({ total: total, count: count });
345
+ } else {
346
+ return t.all({ total: total, count: count });
347
+ }
348
+ }
349
+ },
350
+
351
+ NEXT_PAGE_THRESHOLD: 600,
352
+
353
+ // Potentially load more entries on scroll.
354
+ onScroll: _.debounce(function(e) {
355
+ if (this.loading) return;
356
+ if (this.inUploadView) return;
357
+
358
+ var library = this.$('.library'),
359
+ scrollTop = this.scrollArea.scrollTop(),
360
+ scrollBottom = scrollTop + this.scrollArea.height(),
361
+ remaining = library.height() - scrollBottom;
362
+
363
+ if (remaining <= this.NEXT_PAGE_THRESHOLD && this.collection.hasMorePages()) {
364
+ this.showLoadingSpinner();
365
+ this.fetch({ add: true });
366
+ }
367
+ }, 300),
368
+
369
+ // Spinner graphic, revealed as necessary.
370
+ loadingSpinner: _.memoize(function() {
371
+ return $(
372
+ '<li class="loading-indicator">' +
373
+ '<span class="label">Loading&hellip;</label>' +
374
+ '</li>'
375
+ ).appendTo(this.$('.library'));
376
+ }),
377
+
378
+ showLoadingSpinner: function() {
379
+ this.loadingSpinner().css({ display: 'block' });
380
+ },
381
+
382
+ hideLoadingSpinner: function() {
383
+ this.loadingSpinner().css({ display: 'none' });
384
+ },
385
+
386
+ // Wrapper around the collection’s fetch method. Takes seach
387
+ // and paging into account.
388
+ fetch: function(options) {
389
+ options = _.extend({ add: false }, options);
390
+
391
+ this.loading = true;
392
+
393
+ this.exitUploadView();
394
+ this.showLoadingSpinner();
395
+
396
+ if (!options.add) this.collection.reset();
397
+
398
+ this.collection.fetch({
399
+ data: {
400
+ search: this.searchTerm(),
401
+ page: options.add ? (this.collection.currentPage + 1) : 1
402
+ },
403
+ add: options.add,
404
+ success: this.onFetchSuccess
405
+ });
406
+ },
407
+
408
+ // Hide spinner and set loading state back to false.
409
+ onFetchSuccess: function() {
410
+ this.loading = false;
411
+ this.hideLoadingSpinner();
412
+ this.updateCount();
413
+ },
414
+
415
+ // ## Drag-Drop API:
416
+ //
417
+ // Any object can hook into AssetLibrary’s drag-drop API by
418
+ // implementing the following protocol:
419
+ //
420
+ // Bind to the `assets:dragStarted` event and register to receive assets:
421
+ //
422
+ // var self = this;
423
+ //
424
+ // $(window).on('assets:dragStarted', function(event, library) {
425
+ // library.registerReceiver(self);
426
+ // });
427
+ //
428
+ // Implement the `withinBounds` method:
429
+ //
430
+ // this.withinBounds = function(x, y) {
431
+ // // Return true or false
432
+ // }
433
+ //
434
+ // Implement the `assetsOver` method:
435
+ //
436
+ // this.assetsOver = function(x, y) { ... }
437
+ //
438
+ // Implement the `assetsNotOver` method:
439
+ //
440
+ // this.assetsNotOver = function() { ... }
441
+ //
442
+ // Implement the `receiveAssets` method:
443
+ //
444
+ // this.receiveAssets = function(assets, x, y) { ... }
445
+ //
446
+
447
+ // Store the originating click, and bind onto relevant mouse events.
448
+ prepareForDrag: function(e) {
449
+ $(document).on('mousemove', this.onDrag).
450
+ on('mouseup', this.stopDrag);
451
+
452
+ this.dragOrigin = { x: e.pageX, y: e.pageY };
453
+ },
454
+
455
+ // If we’ve already started dragging, update the little helper so it follows
456
+ // the user’s mouse pointer around. Otherwise, test if we’ve crossed the
457
+ // threshold yet.
458
+ onDrag: function(e) {
459
+ if (this.dragging) {
460
+ this.dragHelper.css({ left: e.pageX + 'px', top: e.pageY + 'px' });
461
+ this.reviewReceiversForDrag(e.pageX, e.pageY);
462
+ } else {
463
+ this.testDragTreshold(e);
464
+ }
465
+ },
466
+
467
+ // Dragging only begins when the mouse is dragged over DRAG_TRESHOLD.
468
+ testDragTreshold: function(e) {
469
+ var absDelta = Math.max(
470
+ Math.abs(e.pageX - this.dragOrigin.x),
471
+ Math.abs(e.pageY - this.dragOrigin.y)
472
+ );
473
+
474
+ if (absDelta >= this.DRAG_THRESHOLD) this.startDrag(e);
475
+ },
476
+
477
+ // Set the dragging state to true and create the drag helper -
478
+ // the collection of little thumbs representing the current selection.
479
+ startDrag: function(e) {
480
+ this.dragging = true;
481
+
482
+ this.dragHelper = $('<span class="asset-drag-helper">');
483
+ this.dragHelper.css({ position: 'absolute' });
484
+
485
+ for (var i in this.selection) { var thumb = this.selection[i];
486
+ var img = thumb.$('img').clone(false);
487
+ this.dragHelper.append(img);
488
+ }
489
+
490
+ this.dragHelper.appendTo('body');
491
+
492
+ var width = Math.ceil(Math.sqrt(this.selection.length)) *
493
+ (this.dragHelper.width() / this.selection.length);
494
+
495
+ this.dragHelper.width(width);
496
+
497
+ this.receivers = [this];
498
+
499
+ $(window).trigger('assets:dragStarted', this);
500
+
501
+ window.autoscroll.start();
502
+
503
+ this.onDrag(e);
504
+ },
505
+
506
+ // Unbind mousey events, delete the drag helper, and inform potenital
507
+ // recipients that a drop is taking place.
508
+ stopDrag: function(e) {
509
+ $(document).off('mousemove', this.onDrag).
510
+ off('mouseup', this.stopDrag);
511
+
512
+ window.autoscroll.stop();
513
+
514
+ if (this.dragHelper) {
515
+ this.dragHelper.remove();
516
+ delete this.dragHelper;
517
+ }
518
+
519
+ delete this.dragging;
520
+
521
+ this.reviewReceiversForDrop(e.pageX, e.pageY);
522
+ delete this.receivers;
523
+ },
524
+
525
+ // Registers a potential asset receiver. Receivers are polled in order.
526
+ registerReceiver: function(receiver) {
527
+ this.receivers.push(receiver);
528
+ },
529
+
530
+ // On drag, we’ll want to send `assetsNotOver` to all receivers,
531
+ // then identify the receiver under the cursor (if any) and it
532
+ // `assetsOver`.
533
+ reviewReceiversForDrag: function(x, y) {
534
+ var receiver = this.identifyReceiver(x, y);
535
+
536
+ _.each(this.receivers, function(r) {
537
+ if (r !== receiver) r.assetsNotOver();
538
+ else r.assetsOver(x, y);
539
+ });
540
+ },
541
+
542
+ // On drop, we’ll identify the receiver under the cursor and send
543
+ // `receiveAssets`, with the current selection.
544
+ reviewReceiversForDrop: function(x, y) {
545
+ _.invoke(this.receivers, 'assetsNotOver');
546
+ var receiver = this.identifyReceiver(x, y);
547
+ if (receiver) receiver.receiveAssets(this.selectedAssets(), x, y);
548
+ },
549
+
550
+ // Find the first receiver in the stack for which `withinBounds`
551
+ // returns true.
552
+ identifyReceiver: function(x, y) {
553
+ return _.find(this.receivers, function(receiver) {
554
+ return receiver.withinBounds(x, y);
555
+ });
556
+ },
557
+
558
+ // Internal implementation of asset-receiver interface.
559
+ //
560
+ // The library itself is first on the stack of receivers.
561
+ // So, if an asset payload is over the library,
562
+ // it will get be the identified receiver.
563
+
564
+ withinBounds: function(x, y) {
565
+ var offset = $(this.el).offset(),
566
+ top = offset.top,
567
+ left = offset.left,
568
+ bottom = top + $(this.el).height(),
569
+ right = left + $(this.el).width();
570
+
571
+ return x >= left && x <= right && y >= top && y <= bottom;
572
+ },
573
+
574
+ assetsOver: function() {},
575
+ assetsNotOver: function() {},
576
+ receiveAssets: function() {},
577
+
578
+ keydown: function(e) {
579
+ if (this.selection.length > 0 && e.which === 8) {
580
+ e.preventDefault();
581
+ e.stopImmediatePropagation();
582
+ this.destroySelection();
583
+ }
584
+ },
585
+
586
+ // Destroy the selected assets. This sends 'destroy' to each asset
587
+ // indvidually. If the responses to the DELETE requests are a bit slow,
588
+ // then this can look a little inelegant.
589
+ destroySelection: function() {
590
+ if (this.selection.length === 1) {
591
+ var message = 'Are you sure you want to delete this asset?';
592
+ } else {
593
+ var message = Handlebars.compile(
594
+ 'Are you sure you want to delete these {{count}} assets?'
595
+ )({ count: this.selection.length });
596
+ }
597
+
598
+ if (confirm(message)) {
599
+ _.invoke(this.selectedAssets(), 'destroy');
600
+ }
601
+ },
602
+
603
+ // When this mode is switched on, AssetLibrary will listen out globally
604
+ // for keydown events; primarily to catch the delete key at the moment.
605
+ //
606
+ // If a click is encountered outside the area of the library, we jump
607
+ // out of this mode.
608
+ listenForKeys: function() {
609
+ if (this.listeningForKeys) return;
610
+ this.listeningForKeys = true;
611
+ $(window).on('keydown', this.keydown);
612
+ $(window).one('mousedown', this.stopListeningForKeys);
613
+ },
614
+
615
+ // Jump out of keydown-aware mode.
616
+ stopListeningForKeys: function() {
617
+ this.listeningForKeys = false;
618
+ $(window).off('keydown', this.keydown);
619
+ },
620
+
621
+ startResize: function(e) {
622
+ e.preventDefault();
623
+ e.stopImmediatePropagation();
624
+
625
+ this.resizeOrigin = { y: e.pageY, h: this.$el.height() };
626
+
627
+ $(window).on('mousemove', this.performResize).
628
+ on('mouseup', this.endResize);
629
+ },
630
+
631
+ performResize: function(e) {
632
+ var delta = e.pageY - this.resizeOrigin.y,
633
+ height = Math.max(this.resizeOrigin.h - delta, this.DRAWER_HEIGHT);
634
+
635
+ this.$el.height(height);
636
+ },
637
+
638
+ endResize: function(e) {
639
+ $('#container').css({ paddingBottom: this.$el.height() + 'px' });
640
+ $(window).off('mousemove', this.performResize).
641
+ off('mouseup', this.endResize);
642
+ },
643
+
644
+ // Enables upload-to-library functionality.
645
+ initializeUploader: function() {
646
+ if (this.uploader) return;
647
+
648
+ this.uploader = new slices.Uploader({
649
+ button : this.$el.find('[data-action="upload"]'),
650
+ drop : this.$el.find('.library-container')
651
+ });
652
+ this.uploader.on('filesAdded', this.onFilesAdded);
653
+ this.uploader.on('fileUploaded', this.onFileUploaded);
654
+ },
655
+
656
+ // When files are added to the upload queue we create corresponding
657
+ // asset objects and add them to the collection.
658
+ onFilesAdded: function(event) {
659
+ this.enterUploadView();
660
+
661
+ _(event.files).each(function(file) {
662
+ var a = new slices.Asset({ file: file });
663
+ // This is clearly a code-smell!
664
+ file.asset = a;
665
+ // These bits are fine.
666
+ this.collection.add(a);
667
+ this.updateFileStatus(file);
668
+ }, this);
669
+
670
+ this.uploader.start();
671
+ },
672
+
673
+ // This looks weird, I know, but really all we’re doing is taking the
674
+ // response from our upload to /assets and feeding it into our
675
+ // asset model.
676
+ //onFileUploaded: function(uploader, file, transport) {
677
+ onFileUploaded: function(event) {
678
+ var file = event.file,
679
+ response = event.response;
680
+
681
+ // Update the attachment model. Silently, because we want to control
682
+ // how it redraws.
683
+ var asset = file.asset;
684
+ asset.set(response);
685
+ // Finally complete upload progress display and transition to thumbnail.
686
+ this.viewForFile(file).updateFileAndComplete(file);
687
+ },
688
+
689
+ // When uploading files we just display the new uploads. All this does is
690
+ // clear the current collection. There's also a catch in there to ensure
691
+ // we don’t accidentally clear the collection when we don’t mean to.
692
+ enterUploadView: function() {
693
+ if (this.inUploadView) return;
694
+ this.inUploadView = true;
695
+ this.collection.reset();
696
+ this.$('[type="search"]').hide();
697
+ this.updateCount('Uploading…');
698
+ },
699
+
700
+ // When we’re no longer concerned with just our uploaded files, we return
701
+ // to normal.
702
+ exitUploadView: function() {
703
+ if (!this.inUploadView) return;
704
+ this.inUploadView = false;
705
+ this.$('[type="search"]').show();
706
+ this.updateCount();
707
+ },
708
+
709
+ // Passes information from upload on to the file’s view.
710
+ updateFileStatus: function(file) {
711
+ this.viewForFile(file).updateFile(file);
712
+ },
713
+
714
+ // Returns the view object associated with the given file.
715
+ viewForFile: function(file) {
716
+ return this.thumbs[file.asset.cid];
717
+ }
718
+
719
+ });
720
+