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,177 @@
1
+ // Date field
2
+ //
3
+ // This shouldn’t be instantiated directly.
4
+ // Instead, use `{{dateField}}` like this:
5
+ //
6
+ // {{dateField field="example"}}
7
+ //
8
+ slices.DateFieldView = Backbone.View.extend({
9
+
10
+ // -- Config --
11
+
12
+ events: {
13
+ 'click .calendar-button' : 'onClickCalendar',
14
+ 'click .clear' : 'onClickClear',
15
+ 'change select' : 'onSelectTime'
16
+ },
17
+
18
+ className: 'date-field',
19
+
20
+ template: Handlebars.compile(
21
+ '<span class="display">' +
22
+ '{{{display}}}' +
23
+ '</span>' +
24
+ '<button class="calendar-button">Choose Date</button>' +
25
+ '<button class="clear">&times;</button>'
26
+ ),
27
+
28
+ displayTemplate: Handlebars.compile(
29
+ '<span class="date">{{date}} at</span> ' +
30
+ '<select class="hour">' +
31
+ '{{#each hours}}<option {{attrs}}>{{value}}</option>{{/each}}' +
32
+ '</select>' +
33
+ ' : ' +
34
+ '<select class="minute">' +
35
+ '{{#each minutes}}<option {{attrs}}>{{value}}</option>{{/each}}' +
36
+ '</select>'
37
+ ),
38
+
39
+ // -- Init --
40
+
41
+ initialize: function() {
42
+ _.bindAll(this);
43
+ this.options.value = this.options.value || null;
44
+ this.value = moment(this.options.value);
45
+ this.prevValue = moment(this.value);
46
+ if (this.options.autoAttach) _.defer(this.attach);
47
+ },
48
+
49
+ // -- Rendering --
50
+
51
+ placeholder: function() {
52
+ return Handlebars.compile('<div id="placeholder-{{id}}"></div>')(this);
53
+ },
54
+
55
+ attach: function() {
56
+ $('#placeholder-' + this.id).replaceWith(this.el);
57
+ this.render();
58
+ },
59
+
60
+ render: function() {
61
+ this.$el.attr('name', this.options.name);
62
+ this.$el.html(this.template(this));
63
+ this.updateComputedValue();
64
+ this.updateUI();
65
+ this.updateCalendar();
66
+
67
+ if (this.valueHasChanged()) {
68
+ this.prevValue = moment(this.value);
69
+ this.$el.trigger('change');
70
+ }
71
+
72
+ return this;
73
+ },
74
+
75
+ display: function() {
76
+ if (this.value) {
77
+ var self = this;
78
+ return this.displayTemplate({
79
+ date: this.value.format('dddd D MMMM YYYY'),
80
+ hours: _.map(_.range(0, 24), function(hour) {
81
+ var o = { value: _.string.pad('' + hour, 2, '0') };
82
+ if (hour == self.value.hours()) o.attrs = 'selected';
83
+ return o;
84
+ }),
85
+ minutes: _.map(_.range(0, 60, 15), function(minute) {
86
+ var o = { value: _.string.pad('' + minute, 2, '0') };
87
+ if (minute == self.value.minutes()) o.attrs = 'selected';
88
+ return o;
89
+ })
90
+ });
91
+ } else {
92
+ return 'No date set';
93
+ }
94
+ },
95
+
96
+ // -- Event Handlers --
97
+
98
+ onClickCalendar: function(event) {
99
+ event.preventDefault();
100
+ event.stopImmediatePropagation();
101
+ this.toggleCalendar();
102
+ },
103
+
104
+ onClickClear: function(event) {
105
+ event.preventDefault();
106
+ event.stopImmediatePropagation();
107
+ this.value = null;
108
+ this.render();
109
+ },
110
+
111
+ onCalendarChange: function() {
112
+ this.value = moment(this.calendar().value);
113
+ this.render();
114
+ },
115
+
116
+ onSelectTime: function() {
117
+ var hour = parseInt(this.$el.find('.hour').val(), 10),
118
+ minute = parseInt(this.$el.find('.minute').val(), 10);
119
+
120
+ this.value.hours(hour).minutes(minute);
121
+ this.render();
122
+ },
123
+
124
+ // -- Actions --
125
+
126
+ updateComputedValue: function() {
127
+ var computedValue = this.value && this.value.toDate() || null;
128
+ this.$el.data('computed-value', computedValue);
129
+ },
130
+
131
+ updateUI: function() {
132
+ if (this.value) {
133
+ this.$el.find('.clear').show();
134
+ } else {
135
+ this.$el.find('.clear').hide();
136
+ }
137
+ },
138
+
139
+ showCalendar: function() {
140
+ this.calendar().show();
141
+ },
142
+
143
+ hideCalendar: function() {
144
+ this.calendar().hide();
145
+ },
146
+
147
+ toggleCalendar: function() {
148
+ this.calendar().toggle();
149
+ },
150
+
151
+ updateCalendar: function() {
152
+ this.calendar().anchor = this.$el.find('.calendar-button');
153
+ this.calendar().setValue(this.value);
154
+ },
155
+
156
+ // -- Helpers --
157
+
158
+ calendar: function() {
159
+ if (!this._calendar) {
160
+ this._calendar = new slices.CalendarView({
161
+ value: this.value,
162
+ onChange: this.onCalendarChange
163
+ });
164
+ }
165
+ return this._calendar;
166
+ },
167
+
168
+ valueHasChanged: function() {
169
+ return !this.valueHasNotChanged();
170
+ },
171
+
172
+ valueHasNotChanged: function() {
173
+ return (this.value == null && this.prevValue == null) ||
174
+ (this.value && this.value.is(this.prevValue));
175
+ }
176
+
177
+ });
@@ -0,0 +1,142 @@
1
+ // Responsible for the displaying the progress of a current upload.
2
+ slices.FileView = Backbone.View.extend({
3
+
4
+ className: 'upload-info',
5
+
6
+ textTemplate: Handlebars.compile(
7
+ '<span class="upload-name">{{name}}</span>' +
8
+ '<span class="upload-summary">{{summary}}</span>'
9
+ ),
10
+
11
+ imagePath: Handlebars.compile('/assets/slices/icon_upload_{{name}}.png'),
12
+
13
+ possibleStatusList: [
14
+ 'status-queued',
15
+ 'status-uploading',
16
+ 'status-processing',
17
+ 'status-done',
18
+ 'status-failed'
19
+ ].join(' '),
20
+
21
+ // Create and append canvas and text then bind up the model.
22
+ initialize: function() {
23
+ _.bindAll(this);
24
+ this.canvas = $('<canvas class="canvas" width="180" height="180" />');
25
+ this.text = $('<div class="text" />');
26
+ $(this.el).append(this.canvas, this.text);
27
+ this.model.bind('change', this.render);
28
+ this.preloadImages();
29
+ this.render();
30
+ },
31
+
32
+ // Update the canvas to match file status and text with the current details.
33
+ render: function() {
34
+ this.updateCanvas();
35
+ this.updateText();
36
+ },
37
+
38
+ // Updates the canvas to display either the upload progress, if currently
39
+ // uploading, or one of our state indicators:
40
+ // waiting, thinking, happy, or sad.
41
+ updateCanvas: function() {
42
+ switch (this.model.status()) {
43
+ case 'queued' : this.drawProgress(0); break;
44
+ case 'uploading' : this.drawProgress(this.model.progress()); break;
45
+ case 'processing' : this.drawImage('thinking'); break;
46
+ case 'done' : this.drawImage('happy'); break;
47
+ case 'failed' : this.drawImage('sad'); break;
48
+ }
49
+ },
50
+
51
+ // Updates the textual information to reflect the current state of our file.
52
+ updateText: function() {
53
+ this.text.html(this.textTemplate(this));
54
+ },
55
+
56
+ // Name value presented for textTemplate. Yes, our view class is also
57
+ // acting as a presenter, deal with it.
58
+ name: function() {
59
+ return this.model.get('name');
60
+ },
61
+
62
+ // Summary value presented for textTemplate.
63
+ summary: function() {
64
+ switch (this.model.status()) {
65
+ case 'queued' : return 'Waiting…';
66
+ case 'uploading' : return this.progressSummary();
67
+ case 'processing' : return 'Thinking…';
68
+ case 'done' : return 'Done!';
69
+ case 'failed' : return 'Failed'
70
+ }
71
+ },
72
+
73
+ // Returns a nicely formatted '826KB of 3.2MB' style summary.
74
+ progressSummary: function() {
75
+ return humanFileSize(this.model.get('loaded')) + ' of ' +
76
+ humanFileSize(this.model.get('total'));
77
+ },
78
+
79
+ // Draw progress pie.
80
+ drawProgress: function(progress) {
81
+ var ctx = this.canvas[0].getContext('2d'),
82
+ t = -Math.PI / 2,
83
+ w = 180,
84
+ h = 180,
85
+ x = 90,
86
+ y = 75,
87
+ r = 30,
88
+ p = 3;
89
+
90
+ ctx.fillStyle = '#ddd';
91
+ ctx.moveTo(0, 0);
92
+ ctx.fillRect(0, 0, w, h);
93
+
94
+ ctx.beginPath();
95
+ ctx.fillStyle = '#838383';
96
+ ctx.moveTo(x, y);
97
+ ctx.arc(x, y, r, 0, Math.PI * 2, false);
98
+ ctx.closePath();
99
+ ctx.fill();
100
+
101
+ ctx.beginPath();
102
+ ctx.fillStyle = '#ddd';
103
+ ctx.moveTo(x, y);
104
+
105
+ if (progress < 1) var a = t - (Math.PI * 2 * (1 - progress));
106
+ else var a = t + (Math.PI * 2);
107
+
108
+ ctx.arc(x, y, r - p, t, a, false);
109
+ ctx.closePath();
110
+ ctx.fill();
111
+ },
112
+
113
+ // Draw progress image. Available names are:
114
+ // 'thinking', 'happy' & 'sad'.
115
+ drawImage: function(name) {
116
+ var ctx = this.canvas[0].getContext('2d');
117
+
118
+ var img = new Image();
119
+
120
+ img.onload = function() {
121
+ ctx.fillStyle = '#ddd';
122
+ ctx.moveTo(0, 0);
123
+ ctx.fillRect(0, 0, 180, 180);
124
+ ctx.moveTo(0, 0);
125
+ ctx.drawImage(img, 0, 0);
126
+ };
127
+
128
+ img.src = this.imagePath({ name: name });
129
+ },
130
+
131
+ remove: function() {
132
+ $(this.el).remove();
133
+ },
134
+
135
+ preloadImages: function() {
136
+ _(['thinking', 'happy', 'sad']).each(_.bind(function(name) {
137
+ var image = new Image();
138
+ image.src = slices.UPLOAD_ICONS[name];
139
+ }, this));
140
+ }
141
+
142
+ });
@@ -0,0 +1,253 @@
1
+ (function() {
2
+
3
+ // Tokenized field, used for tags, categories etc.
4
+ //
5
+ // This shouldn’t be instantiated directly.
6
+ // Instead, use `{{tokenField}}` like this:
7
+ //
8
+ // {{tokenField field="categories" options="available_categories"}}
9
+ //
10
+
11
+ var ESC = 27,
12
+ ENTER = 13,
13
+ BACKSPACE = 8,
14
+ UP = 38,
15
+ DOWN = 40,
16
+ COMMA = 188,
17
+ TAB = 9;
18
+
19
+ slices.TokenFieldView = Backbone.View.extend({
20
+
21
+ // -- Config --
22
+
23
+ events: {
24
+ 'keydown' : 'onKey',
25
+ 'keypress' : 'onKey',
26
+ 'click .del' : 'onClickDel',
27
+ 'click .option' : 'onClickOption'
28
+ },
29
+
30
+ className: 'token-field',
31
+
32
+ template: Handlebars.compile(
33
+ '<input type="text" class="input" name="{{id}}">' +
34
+ '<span class="tokens">' +
35
+ '{{#each values}}' +
36
+ '<span class="token" data-value="{{this}}" unselectable="true">{{this}} <span class="del">&times;</span></span>' +
37
+ '{{/each}}' +
38
+ '</span>' +
39
+ '<span class="options">' +
40
+ '{{#each valueOptions}}' +
41
+ '<a href="#" class="option">{{this}}</a>' +
42
+ '{{/each}}' +
43
+ '</span>'
44
+ ),
45
+
46
+ // -- Init --
47
+
48
+ initialize: function() {
49
+ _.bindAll(this);
50
+
51
+ this.values = this.sanitizeValues(this.options.values);
52
+ this.prevValues = [].concat(this.values || []);
53
+ this.valueOptions = [].concat(this.options.options || []);
54
+
55
+ $(document).on('slices:willSubmit', this.capture);
56
+
57
+ if (this.options.autoAttach) _.defer(this.attach);
58
+ },
59
+
60
+ // -- Rendering --
61
+
62
+ placeholder: function() {
63
+ return Handlebars.compile('<div id="placeholder-{{id}}"></div>')(this);
64
+ },
65
+
66
+ attach: function() {
67
+ $('#placeholder-' + this.id).replaceWith(this.el);
68
+ this.render();
69
+ },
70
+
71
+ render: function() {
72
+ this.prepareValueOptions();
73
+
74
+ this.$el.html(this.template(this));
75
+
76
+ this.updateComputedValue();
77
+ this.updateValueOptions();
78
+ this.updateLayout();
79
+ this.updateInput();
80
+
81
+ if (_.isEqual(this.values, this.prevValues)) return;
82
+
83
+ this.$el.find('input').trigger('change');
84
+ this.prevValues = [].concat(this.values);
85
+ },
86
+
87
+ // -- Commands --
88
+
89
+ focus: function() {
90
+ this.$el.find('input').focus();
91
+ },
92
+
93
+ capture: function() {
94
+ var newValues = this.$el.find('input').val().split(/ *, */);
95
+
96
+ this.values = this.sanitizeValues(this.values.concat(newValues));
97
+
98
+ this.render();
99
+ this.focus();
100
+ },
101
+
102
+ delayedCapture: _.debounce(function() {
103
+ this.capture();
104
+ }, 750),
105
+
106
+ focusOrRemoveLastToken: function(e) {
107
+ var focusedToken = this.$el.find('.token.focus');
108
+
109
+ if (focusedToken.length) {
110
+ this.values.pop();
111
+ this.render();
112
+ this.focus();
113
+ } else {
114
+ this.$el.find('.token:last').addClass('focus');
115
+ }
116
+ },
117
+
118
+ updateLayout: function() {
119
+ var tokens = this.$el.find('.tokens'),
120
+ input = this.$el.find('input');
121
+
122
+ if (!this.hasOwnProperty('_paddingLeft')) {
123
+ this._paddingLeft = input.css('paddingLeft');
124
+ }
125
+
126
+ if (this.values.length) {
127
+ input.css({ paddingLeft: tokens.outerWidth() + 'px' });
128
+ } else {
129
+ input.css({ paddingLeft: this._paddingLeft });
130
+ }
131
+ },
132
+
133
+ updateInput: function() {
134
+ if (this.options.singular && this.values.length) {
135
+ this.$el.find('input').attr('disabled', true);
136
+ }
137
+ },
138
+
139
+ updateValueOptions: function() {
140
+ var self = this;
141
+
142
+ this.$el.find('.option').each(function() {
143
+ var option = $(this),
144
+ value = option.text();
145
+
146
+ if (_.contains(self.values, value)) {
147
+ $(this).addClass('used');
148
+ } else {
149
+ $(this).removeClass('used');
150
+ }
151
+ });
152
+ },
153
+
154
+ updateComputedValue: function() {
155
+ if (this.options.singular) {
156
+ var computedValue = this.values[0];
157
+ } else {
158
+ var computedValue = this.values;
159
+ }
160
+
161
+ this.$el.data('computed-value', computedValue || null);
162
+ },
163
+
164
+ // -- Event Handlers --
165
+
166
+ onKey: function(e) {
167
+ switch (e.which) {
168
+ case ENTER:
169
+ case COMMA:
170
+ case TAB:
171
+ this.onTokenBreaker(e); break;
172
+ case BACKSPACE:
173
+ this.onBackspace(e); break;
174
+ default:
175
+ this.delayedCapture();
176
+ }
177
+ },
178
+
179
+ onTokenBreaker: function(e) {
180
+ var input = this.$el.find('input'),
181
+ value = input.val();
182
+
183
+ if (value.length) {
184
+ e.preventDefault();
185
+ this.capture();
186
+ }
187
+ },
188
+
189
+ onBackspace: function(e) {
190
+ var input = this.$el.find('input'),
191
+ value = input.val();
192
+
193
+ if (value.length) {
194
+ this.delayedCapture();
195
+ } else {
196
+ e.preventDefault();
197
+ this.focusOrRemoveLastToken();
198
+ }
199
+ },
200
+
201
+ onClickDel: function(e) {
202
+ e.preventDefault();
203
+
204
+ var token = $(e.target).closest('.token'),
205
+ value = token.data('value');
206
+
207
+ this.values = _.without(this.values, value);
208
+ this.render();
209
+ this.focus();
210
+ },
211
+
212
+ onClickOption: function(e) {
213
+ e.preventDefault();
214
+
215
+ var option = $(e.target),
216
+ value = option.text();
217
+
218
+ if (this.options.singular) {
219
+ this.values = [value];
220
+ } else {
221
+ if (_.contains(this.values, value)) {
222
+ this.values = _.without(this.values, value);
223
+ } else {
224
+ this.values.push(value);
225
+ }
226
+ }
227
+
228
+ this.render();
229
+ },
230
+
231
+ // -- Helpers --
232
+
233
+ sanitizeValues: function(values) {
234
+ values = [values];
235
+ values = _.flatten(values);
236
+ values = _.map(values, function(token) {
237
+ return token && token.replace(/^\s+/, '').replace(/\s+$/, '');
238
+ });
239
+ values = _.compact(values);
240
+ values = _.uniq(values);
241
+
242
+ if (this.options.singular) values = values.slice(0, 1);
243
+
244
+ return values;
245
+ },
246
+
247
+ prepareValueOptions: function() {
248
+ this.valueOptions = _.uniq(this.valueOptions.concat(this.values)).sort();
249
+ }
250
+
251
+ });
252
+
253
+ })();