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