outpost-aggregator 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.
- data/.gitignore +17 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +117 -0
- data/Rakefile +14 -0
- data/config.ru +7 -0
- data/lib/assets/javascripts/outpost/aggregator/aggregator.js.coffee +796 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/_pagination.jst.eco +12 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/base.jst.eco +90 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/content_full.jst.eco +19 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/content_small.jst.eco +15 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/drop_zone.jst.eco +1 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/error.jst.eco +4 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/recent_content.jst.eco +2 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/search.jst.eco +6 -0
- data/lib/assets/javascripts/outpost/aggregator/templates/url.jst.eco +5 -0
- data/lib/assets/javascripts/outpost/aggregator.js +2 -0
- data/lib/assets/stylesheets/outpost/aggregator.css.scss +93 -0
- data/lib/outpost/aggregator/json_input.rb +75 -0
- data/lib/outpost/aggregator/simple_json.rb +12 -0
- data/lib/outpost/aggregator/version.rb +5 -0
- data/lib/outpost/aggregator.rb +21 -0
- data/outpost-aggregator.gemspec +28 -0
- data/spec/factories.rb +2 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/outpost/aggregator/json_input_spec.rb +4 -0
- data/spec/lib/outpost/aggregator/simple_json_spec.rb +4 -0
- data/spec/spec_helper.rb +23 -0
- metadata +204 -0
@@ -0,0 +1,796 @@
|
|
1
|
+
##
|
2
|
+
# outpost.Aggregator
|
3
|
+
#
|
4
|
+
# Hooks into ContentAPI to help YOU, our loyal
|
5
|
+
# customer, aggregate content for various
|
6
|
+
# purposes
|
7
|
+
#
|
8
|
+
# Made up of basically two parts:
|
9
|
+
# * The "DropZone", where content will be dropped
|
10
|
+
# and sorted and generally managed.
|
11
|
+
#
|
12
|
+
# * The "Content Finder" area, where the user can easily
|
13
|
+
# find content by searching, selecting
|
14
|
+
# from the recent content, or dropping in a URL.
|
15
|
+
#
|
16
|
+
class outpost.Aggregator
|
17
|
+
|
18
|
+
@TemplatePath = "outpost/aggregator/templates/"
|
19
|
+
|
20
|
+
#---------------------
|
21
|
+
|
22
|
+
defaults:
|
23
|
+
apiType: "public"
|
24
|
+
params: {}
|
25
|
+
viewOptions: {}
|
26
|
+
|
27
|
+
constructor: (el, input, json, options={}) ->
|
28
|
+
@options = _.defaults options, @defaults
|
29
|
+
|
30
|
+
@el = $(el)
|
31
|
+
@input = $(input)
|
32
|
+
|
33
|
+
# Set the type of API we're dealing with
|
34
|
+
apiClass = if @options.apiType is "public" then "ContentCollection" else "PrivateContentCollection"
|
35
|
+
|
36
|
+
@baseView = new outpost.Aggregator.Views.Base _.extend options.view || {},
|
37
|
+
el: @el
|
38
|
+
collection: new outpost.ContentAPI[apiClass](json)
|
39
|
+
input: @input
|
40
|
+
apiClass: apiClass
|
41
|
+
params: @options.params
|
42
|
+
viewOptions: @options.viewOptions
|
43
|
+
|
44
|
+
@baseView.render()
|
45
|
+
|
46
|
+
|
47
|
+
#----------------------------------
|
48
|
+
# Views!
|
49
|
+
class @Views
|
50
|
+
#----------------------------------
|
51
|
+
# The skeleton for the the different pieces!
|
52
|
+
class @Base extends Backbone.View
|
53
|
+
template: JST[Aggregator.TemplatePath + 'base']
|
54
|
+
defaults:
|
55
|
+
active: "recent"
|
56
|
+
|
57
|
+
#---------------------
|
58
|
+
|
59
|
+
initialize: ->
|
60
|
+
@options = _.defaults @options, @defaults
|
61
|
+
|
62
|
+
# @foundCollection is the collection for all the content
|
63
|
+
# in the RIGHT panel.
|
64
|
+
@foundCollection = new outpost.ContentAPI[@options.apiClass]()
|
65
|
+
|
66
|
+
#---------------------
|
67
|
+
# Import a URL and turn it into content
|
68
|
+
# Let the caller handle what happens after the request
|
69
|
+
# via callbacks
|
70
|
+
importUrl: (url, callbacks={}) ->
|
71
|
+
$.getJSON(
|
72
|
+
outpost.ContentAPI[@options.apiClass].prototype.url + "/by_url",
|
73
|
+
_.extend @options.params, { url: url })
|
74
|
+
.success((data, textStatus, jqXHR) -> callbacks.success?(data))
|
75
|
+
.error((jqXHR, textStatus, errorThrown) -> callbacks.error?(jqXHR))
|
76
|
+
.complete((jqXHR, status) -> callbacks.complete?(jqXHR))
|
77
|
+
|
78
|
+
true
|
79
|
+
|
80
|
+
#---------------------
|
81
|
+
|
82
|
+
render: ->
|
83
|
+
# Build the skeleton. We'll fill everything in next.
|
84
|
+
@$el.html @template(active: @options.active)
|
85
|
+
|
86
|
+
# Build each of the tabs
|
87
|
+
@recentContent = new outpost.Aggregator.Views.RecentContent(base: @)
|
88
|
+
@search = new outpost.Aggregator.Views.Search(base: @)
|
89
|
+
@url = new outpost.Aggregator.Views.URL(base: @)
|
90
|
+
|
91
|
+
# Build the Drop Zone section
|
92
|
+
@dropZone = new outpost.Aggregator.Views.DropZone
|
93
|
+
collection: @collection # The bootstrapped content
|
94
|
+
base: @
|
95
|
+
|
96
|
+
@
|
97
|
+
|
98
|
+
|
99
|
+
#----------------------------------
|
100
|
+
# The drop-zone!
|
101
|
+
# Gets filled with ContentFull views
|
102
|
+
class @DropZone extends Backbone.View
|
103
|
+
template: JST[Aggregator.TemplatePath + 'drop_zone']
|
104
|
+
container: "#aggregator-dropzone"
|
105
|
+
tagName: 'ul'
|
106
|
+
attributes:
|
107
|
+
class: "drop-zone well"
|
108
|
+
|
109
|
+
# Define alerts as functions
|
110
|
+
@Alerts:
|
111
|
+
success: (el, data) ->
|
112
|
+
new outpost.Notification(el, "success",
|
113
|
+
"<strong>Success!</strong> Imported #{data.id}")
|
114
|
+
|
115
|
+
alreadyExists: (el) ->
|
116
|
+
new outpost.Notification(el, "warning",
|
117
|
+
"That content is already in the drop zone.")
|
118
|
+
|
119
|
+
invalidUrl: (el, url) ->
|
120
|
+
new outpost.Notification(el, "error",
|
121
|
+
"<strong>Failure.</strong> Invalid URL (#{url})")
|
122
|
+
|
123
|
+
error: (el) ->
|
124
|
+
new outpost.Notification(el, "error",
|
125
|
+
"<strong>Error.</strong> Try the Search tab.")
|
126
|
+
|
127
|
+
#---------------------
|
128
|
+
|
129
|
+
initialize: ->
|
130
|
+
@base = @options.base
|
131
|
+
|
132
|
+
# Setup the container, render the template,
|
133
|
+
# and then add in the el (the list)
|
134
|
+
@container = $(@container)
|
135
|
+
@container.html @template
|
136
|
+
@container.append @$el
|
137
|
+
@helper = $("<h1 />").html("Drop Content Here")
|
138
|
+
|
139
|
+
@render()
|
140
|
+
|
141
|
+
# Register listeners for URL droppage
|
142
|
+
@dragOver = false
|
143
|
+
@$el.on "dragenter", (event) => @_dragEnter(event)
|
144
|
+
@$el.on "dragleave", (event) => @_dragLeave(event)
|
145
|
+
@$el.on "dragover", (event) => @_dragOver(event)
|
146
|
+
@$el.on "drop", (event) => @importUrl(event)
|
147
|
+
|
148
|
+
# Listeners for @collection events triggered
|
149
|
+
# by Backbone
|
150
|
+
@collection.bind "add remove reorder", =>
|
151
|
+
@checkDropZone()
|
152
|
+
@setPositions()
|
153
|
+
@updateInput()
|
154
|
+
|
155
|
+
# DropZone callbacks!!
|
156
|
+
sortIn = true
|
157
|
+
dropped = false
|
158
|
+
|
159
|
+
@$el.sortable
|
160
|
+
# When dragging (sorting) starts
|
161
|
+
start: (event, ui) ->
|
162
|
+
sortIn = true
|
163
|
+
dropped = false
|
164
|
+
ui.item.addClass("dragging")
|
165
|
+
|
166
|
+
# Called whenever an item is moved and is over the
|
167
|
+
# DropZone.
|
168
|
+
over: (event, ui) ->
|
169
|
+
sortIn = true
|
170
|
+
ui.item.addClass("adding")
|
171
|
+
ui.item.removeClass("removing")
|
172
|
+
|
173
|
+
# This one gets called both when the item moves out of
|
174
|
+
# the dropzone, AND when the item is dropped inside of
|
175
|
+
# the dropzone. I don't know why jquery-ui decided to
|
176
|
+
# make it this way, but we have to hack around it.
|
177
|
+
out: (event, ui) =>
|
178
|
+
# If this isn't a "drop" event, we can assume that
|
179
|
+
# the item was just moved out of the DropZone.
|
180
|
+
#
|
181
|
+
# If that's the case, and the item was originally
|
182
|
+
# in the dropzone, then add the "removing" class.
|
183
|
+
# Also stop any animation immediately.
|
184
|
+
#
|
185
|
+
# If "drop event" is the case but the element came
|
186
|
+
# from somewhere else, then don't add the "removing"
|
187
|
+
# class.
|
188
|
+
if !dropped && ui.sender[0] == @$el[0]
|
189
|
+
sortIn = false
|
190
|
+
ui.item.stop(false, true)
|
191
|
+
ui.item.addClass("removing")
|
192
|
+
|
193
|
+
ui.item.removeClass("adding")
|
194
|
+
|
195
|
+
# When dragging (sorting) stops, only if the item
|
196
|
+
# being dragged belongs to the original list
|
197
|
+
# Before placeholder disappears
|
198
|
+
beforeStop: (event, ui) =>
|
199
|
+
dropped = true
|
200
|
+
|
201
|
+
# When an item from another list is dropped into this
|
202
|
+
# DropZone
|
203
|
+
# Move it from there to DropZone.
|
204
|
+
receive: (event, ui) =>
|
205
|
+
dropped = true
|
206
|
+
# If we're able to move it in, Remove the dropped
|
207
|
+
# element because we're rendering the bigger, better one.
|
208
|
+
# Otherwise, revert the el back to the original element.
|
209
|
+
if @move(ui.item)
|
210
|
+
ui.item.remove()
|
211
|
+
else
|
212
|
+
$(ui.item).effect 'highlight', color: "#f2dede", 1500
|
213
|
+
$(ui.sender).sortable "cancel"
|
214
|
+
|
215
|
+
# When dragging (sorting) stops, only for items
|
216
|
+
# in the original list.
|
217
|
+
# Update the position attribute for each
|
218
|
+
# model
|
219
|
+
#
|
220
|
+
# If !sortIn (i.e. if we're dragging something out
|
221
|
+
# of the DropZone), then remove that item. A trigger
|
222
|
+
# on @collection.remove() will re-sort the models.
|
223
|
+
#
|
224
|
+
# If we stopped but sortIn is true, then it means
|
225
|
+
# we have just re-ordered the elements in the UI,
|
226
|
+
# so we manually trigger a "reorder" event.
|
227
|
+
stop: (event, ui) =>
|
228
|
+
if !sortIn
|
229
|
+
ui.item.remove()
|
230
|
+
@remove(ui.item)
|
231
|
+
else
|
232
|
+
@collection.trigger "reorder"
|
233
|
+
|
234
|
+
#---------------------
|
235
|
+
|
236
|
+
_stopEvent: (event) ->
|
237
|
+
event.preventDefault()
|
238
|
+
event.stopPropagation()
|
239
|
+
|
240
|
+
#---------------------
|
241
|
+
# When an element enters the zone
|
242
|
+
_dragEnter: (event) ->
|
243
|
+
@_stopEvent event
|
244
|
+
@$el.addClass('dim')
|
245
|
+
|
246
|
+
#---------------------
|
247
|
+
# dragleave has child element problems
|
248
|
+
# When you hover over a child element,
|
249
|
+
# a dragleave event is fired.
|
250
|
+
# So we need to set a small delay to allow
|
251
|
+
# dragover to show dragleave what's up.
|
252
|
+
_dragLeave: (event) ->
|
253
|
+
@dragOver = false
|
254
|
+
setTimeout =>
|
255
|
+
@$el.removeClass('dim') if !@dragOver
|
256
|
+
, 50
|
257
|
+
|
258
|
+
@_stopEvent event
|
259
|
+
|
260
|
+
#---------------------
|
261
|
+
# When an element is in the zone and not yet released
|
262
|
+
# Get continuously and rapidly fired when hovering with
|
263
|
+
# a droppable item.
|
264
|
+
# Set dragOver to true to stop dragleave from messing it up
|
265
|
+
_dragOver: (event) ->
|
266
|
+
@dragOver = true
|
267
|
+
@_stopEvent event
|
268
|
+
|
269
|
+
#---------------------
|
270
|
+
# Proxy to @base.importUrl
|
271
|
+
# Grabs the dropped-in URL, passes it on
|
272
|
+
# Also does some animations and stuff
|
273
|
+
importUrl: (event) ->
|
274
|
+
@_stopEvent event
|
275
|
+
|
276
|
+
@container.spin(zIndex: 1)
|
277
|
+
url = event.originalEvent.dataTransfer.getData('text/uri-list')
|
278
|
+
alert = {}
|
279
|
+
|
280
|
+
@base.importUrl url,
|
281
|
+
success: (data) =>
|
282
|
+
if data
|
283
|
+
if @buildFromData(data)
|
284
|
+
@alert('success', data)
|
285
|
+
else
|
286
|
+
@alert('alreadyExists')
|
287
|
+
else
|
288
|
+
@alert('invalidUrl', url)
|
289
|
+
|
290
|
+
error: (jqXHR) =>
|
291
|
+
@alert('error')
|
292
|
+
|
293
|
+
# Run this no matter what.
|
294
|
+
# It just turns off the bells and whistles
|
295
|
+
complete: (jqXHR) =>
|
296
|
+
@container.spin(false)
|
297
|
+
@$el.removeClass('dim')
|
298
|
+
|
299
|
+
false # prevent default behavior
|
300
|
+
|
301
|
+
#---------------------
|
302
|
+
# Give a JSON object, build a model, and its corresponding
|
303
|
+
# ContentFull view for the DropZone,
|
304
|
+
# then append it to @el and @collection
|
305
|
+
buildFromData: (data) ->
|
306
|
+
model = new outpost.ContentAPI.Content(data)
|
307
|
+
|
308
|
+
# If the model doesn't already exist, then add it,
|
309
|
+
# render it, highlight it
|
310
|
+
# If it does already exist, then just return false
|
311
|
+
if not @collection.get model.id
|
312
|
+
view = new outpost.Aggregator.Views.ContentFull _.extend @base.options.viewOptions,
|
313
|
+
model: model
|
314
|
+
|
315
|
+
@$el.append view.render()
|
316
|
+
@highlightSuccess(view.$el)
|
317
|
+
|
318
|
+
# Add the new model to @collection
|
319
|
+
@collection.add model
|
320
|
+
else
|
321
|
+
false
|
322
|
+
|
323
|
+
#---------------------
|
324
|
+
# Alert the user that the URL drag-and-drop failed or succeeded
|
325
|
+
# Receives a Notification object
|
326
|
+
alert: (alertKey, args...) ->
|
327
|
+
notification = DropZone.Alerts[alertKey](@$el, args...)
|
328
|
+
notification.prepend()
|
329
|
+
|
330
|
+
setTimeout ->
|
331
|
+
notification.fadeOut -> @remove()
|
332
|
+
, 5000
|
333
|
+
|
334
|
+
#---------------------
|
335
|
+
# Moves a model from the "found" section into the drop zone.
|
336
|
+
# Converts its view into a ContentFull view.
|
337
|
+
move: (el) ->
|
338
|
+
id = el.attr("data-id")
|
339
|
+
|
340
|
+
# Get the model for this DOM element
|
341
|
+
# and add it to the DropZone
|
342
|
+
# collection
|
343
|
+
model = @base.foundCollection.get id
|
344
|
+
# If the model is already in @collection, then
|
345
|
+
# let the user know and do not import it
|
346
|
+
# Otherwise, set the position and add it to the collection
|
347
|
+
if not @collection.get id
|
348
|
+
@collection.add model
|
349
|
+
view = new outpost.Aggregator.Views.ContentFull _.extend @base.options.viewOptions,
|
350
|
+
model: model
|
351
|
+
|
352
|
+
el.replaceWith view.render()
|
353
|
+
@highlightSuccess(view.$el)
|
354
|
+
|
355
|
+
else
|
356
|
+
@alert('alreadyExists')
|
357
|
+
false
|
358
|
+
|
359
|
+
#---------------------
|
360
|
+
# Hightlight the el with a success color
|
361
|
+
highlightSuccess: (el) ->
|
362
|
+
el.effect 'highlight', color: "#dff0d8", 1500
|
363
|
+
|
364
|
+
#---------------------
|
365
|
+
# Remove this el's model from @collection
|
366
|
+
# This is the only case where we want to
|
367
|
+
# actually remove a view from @base.childViews
|
368
|
+
remove: (el) ->
|
369
|
+
id = el.attr("data-id")
|
370
|
+
model = @collection.get id
|
371
|
+
@collection.remove model
|
372
|
+
|
373
|
+
#---------------------
|
374
|
+
# Render or hide the "Empty message" for the DropZone,
|
375
|
+
# based on if there is content inside or not
|
376
|
+
checkDropZone: ->
|
377
|
+
if @collection.isEmpty()
|
378
|
+
@_enableDropZoneHelper()
|
379
|
+
else
|
380
|
+
@_disableDropZoneHelper()
|
381
|
+
|
382
|
+
#---------------------
|
383
|
+
# Show the helper, for when there is no content in the dropzone
|
384
|
+
_enableDropZoneHelper: ->
|
385
|
+
@$el.addClass('empty')
|
386
|
+
@$el.append @helper
|
387
|
+
|
388
|
+
#---------------------
|
389
|
+
# Hide the helper, for when there is content in the dropzone
|
390
|
+
_disableDropZoneHelper: ->
|
391
|
+
@$el.removeClass('empty')
|
392
|
+
@helper.detach()
|
393
|
+
|
394
|
+
#---------------------
|
395
|
+
# Go through the li's and find the corresponding model.
|
396
|
+
# This is how we're able to save the order based on
|
397
|
+
# the positions in the DropZone.
|
398
|
+
# Note that this method uses the actual DOM, and
|
399
|
+
# therefore requires that the list has already been
|
400
|
+
# rendered.
|
401
|
+
#
|
402
|
+
# Returns an array of Content (due to some Coffeescript magic)
|
403
|
+
setPositions: ->
|
404
|
+
for el in $("li", @$el)
|
405
|
+
el = $ el
|
406
|
+
id = el.attr("data-id")
|
407
|
+
model = @collection.get id
|
408
|
+
model.set "position", el.index()
|
409
|
+
|
410
|
+
#---------------------
|
411
|
+
# Update the JSON input with current collection
|
412
|
+
updateInput: ->
|
413
|
+
@base.options.input.val(JSON.stringify(@collection.simpleJSON()))
|
414
|
+
|
415
|
+
#---------------------
|
416
|
+
|
417
|
+
render: ->
|
418
|
+
@$el.empty()
|
419
|
+
@checkDropZone()
|
420
|
+
|
421
|
+
# For each model, create a new model view and append it
|
422
|
+
# to the el
|
423
|
+
@collection.each (model) =>
|
424
|
+
view = new outpost.Aggregator.Views.ContentFull _.extend @base.options.viewOptions,
|
425
|
+
model: model
|
426
|
+
|
427
|
+
@$el.append view.render()
|
428
|
+
|
429
|
+
# Set positions.
|
430
|
+
# setPositions depends on the DOM, so it has to be called
|
431
|
+
# after the list has been rendered for it to work.
|
432
|
+
# We assume that the boostrapped content is ordered properly
|
433
|
+
# and can therefore use the DOM to do the ordering and set
|
434
|
+
# the "position" attribute.
|
435
|
+
@setPositions()
|
436
|
+
@
|
437
|
+
|
438
|
+
|
439
|
+
#----------------------------------
|
440
|
+
#----------------------------------
|
441
|
+
# An abstract class from which the different
|
442
|
+
# collection views should inherit
|
443
|
+
class @ContentList extends Backbone.View
|
444
|
+
paginationTemplate: JST[Aggregator.TemplatePath + "_pagination"]
|
445
|
+
errorTemplate: JST[Aggregator.TemplatePath + "error"]
|
446
|
+
events:
|
447
|
+
"click .pagination a": "changePage"
|
448
|
+
|
449
|
+
#---------------------
|
450
|
+
|
451
|
+
initialize: ->
|
452
|
+
@base = @options.base
|
453
|
+
@page = 1
|
454
|
+
@per_page = @base.options.params.limit || 10
|
455
|
+
|
456
|
+
# Grab Recent Content using ContentAPI
|
457
|
+
# Render the list
|
458
|
+
@collection = new outpost.ContentAPI[@base.options.apiClass]()
|
459
|
+
|
460
|
+
# Add just the added model to @base.foundCollection
|
461
|
+
@collection.bind "add", (model, collection, options) =>
|
462
|
+
@base.foundCollection.add model
|
463
|
+
|
464
|
+
# Add the reset collection to @base.foundCollection
|
465
|
+
@collection.bind "reset", (collection, options) =>
|
466
|
+
@base.foundCollection.add collection.models
|
467
|
+
|
468
|
+
@container = $(@container)
|
469
|
+
@container.html @$el
|
470
|
+
|
471
|
+
@render()
|
472
|
+
|
473
|
+
#---------------------
|
474
|
+
# Get the page from the DOM
|
475
|
+
# Proxy to #request to setup params
|
476
|
+
changePage: (event) ->
|
477
|
+
page = parseInt $(event.target).attr("data-page")
|
478
|
+
@request(page: page) if page > 0
|
479
|
+
false # To prevent the link from being followed
|
480
|
+
|
481
|
+
#---------------------
|
482
|
+
# Use this method to fire the request.
|
483
|
+
# Proxies to #_fetch by default.
|
484
|
+
request: (params={}) ->
|
485
|
+
@_fetch(params)
|
486
|
+
|
487
|
+
#---------------------
|
488
|
+
# Private method,
|
489
|
+
# Fire the actual request to the server
|
490
|
+
# Also handles transitions
|
491
|
+
_fetch: (params) ->
|
492
|
+
@transitionStart()
|
493
|
+
|
494
|
+
@collection.fetch
|
495
|
+
data: _.defaults params, @base.options.params
|
496
|
+
success: (collection, response, options) =>
|
497
|
+
# If the collection length is > 0, then
|
498
|
+
# call @renderCollection().
|
499
|
+
# Otherwise render a notice that no results
|
500
|
+
# were found.
|
501
|
+
if collection.length > 0
|
502
|
+
@renderCollection()
|
503
|
+
else
|
504
|
+
@alertNoResults()
|
505
|
+
|
506
|
+
# Set the page and re-render the pagination
|
507
|
+
@renderPagination(params, collection)
|
508
|
+
|
509
|
+
error: (collection, xhr, options) =>
|
510
|
+
@alertError(xhr: xhr)
|
511
|
+
.always => @transitionEnd()
|
512
|
+
|
513
|
+
# Return the collection
|
514
|
+
@collection
|
515
|
+
|
516
|
+
#---------------------
|
517
|
+
# Use this when the aggregator is thinking!
|
518
|
+
# Adds spin and dimming effects
|
519
|
+
transitionStart: ->
|
520
|
+
@resultsEl.addClass('dim')
|
521
|
+
@$el.spin(top: 100, zIndex: 1)
|
522
|
+
|
523
|
+
#---------------------
|
524
|
+
# Use this when the aggregator is done thinking!
|
525
|
+
# Removes spin and dimming effects
|
526
|
+
transitionEnd: ->
|
527
|
+
@resultsEl.removeClass('dim')
|
528
|
+
@$el.spin(false)
|
529
|
+
|
530
|
+
#---------------------
|
531
|
+
|
532
|
+
_stopEvent: (event) ->
|
533
|
+
event.preventDefault()
|
534
|
+
event.stopPropagation()
|
535
|
+
|
536
|
+
#---------------------
|
537
|
+
|
538
|
+
_keypressIsEnter: (event) ->
|
539
|
+
key = event.keyCode || event.which
|
540
|
+
key == 13
|
541
|
+
|
542
|
+
#---------------------
|
543
|
+
# Render a notice if the server returned an error
|
544
|
+
alertError: (options={}) ->
|
545
|
+
xhr = options.xhr
|
546
|
+
|
547
|
+
_.defaults options,
|
548
|
+
el: @resultsEl
|
549
|
+
type: "error"
|
550
|
+
message: @errorTemplate(xhr: xhr)
|
551
|
+
method: "replace"
|
552
|
+
|
553
|
+
alert = new outpost.Notification(options.el,
|
554
|
+
options.type, options.message)
|
555
|
+
|
556
|
+
alert[options.method]()
|
557
|
+
|
558
|
+
#---------------------
|
559
|
+
# Render a notice if no results were returned
|
560
|
+
alertNoResults: (options={}) ->
|
561
|
+
_.defaults options,
|
562
|
+
el: @resultsEl
|
563
|
+
type: "notice"
|
564
|
+
message: "No results"
|
565
|
+
method: "replace"
|
566
|
+
|
567
|
+
alert = new outpost.Notification(options.el,
|
568
|
+
options.type, options.message)
|
569
|
+
|
570
|
+
alert[options.method]()
|
571
|
+
|
572
|
+
#---------------------
|
573
|
+
# Fill in the @resultsEl with the model views
|
574
|
+
renderCollection: ->
|
575
|
+
@resultsEl.empty()
|
576
|
+
|
577
|
+
@collection.each (model) =>
|
578
|
+
view = new outpost.Aggregator.Views.ContentMinimal
|
579
|
+
model: model
|
580
|
+
|
581
|
+
@resultsEl.append view.render()
|
582
|
+
|
583
|
+
@$el
|
584
|
+
|
585
|
+
#---------------------
|
586
|
+
# Re-render the pagination with new page values,
|
587
|
+
# and set @page.
|
588
|
+
#
|
589
|
+
# If the passed-in length is less than the requested
|
590
|
+
# limit, then assume that we reached the end of the
|
591
|
+
# results and disable the "Next" link
|
592
|
+
renderPagination: (params, collection) ->
|
593
|
+
@page = params.page
|
594
|
+
|
595
|
+
# Add in the pagination
|
596
|
+
# Prefer blank classes over "0" for consistency
|
597
|
+
# parseInt(null) and parseInt("") both return null
|
598
|
+
# null compared to any number is always false
|
599
|
+
$(".aggregator-pagination", @$el).html(@paginationTemplate
|
600
|
+
current: @page
|
601
|
+
prev: @page - 1 unless @page < 1
|
602
|
+
next: @page + 1 unless collection.length < params.limit
|
603
|
+
)
|
604
|
+
|
605
|
+
@$el
|
606
|
+
|
607
|
+
#---------------------
|
608
|
+
# Render the whole section.
|
609
|
+
# This should only be called once per page load.
|
610
|
+
# Rendering of indivial collections is done with
|
611
|
+
# @renderCollection().
|
612
|
+
render: ->
|
613
|
+
@$el.html @template
|
614
|
+
@resultsEl = $(@resultsId, @$el)
|
615
|
+
|
616
|
+
# Make the Results div Sortable
|
617
|
+
@resultsEl.sortable
|
618
|
+
connectWith: "#aggregator-dropzone .drop-zone"
|
619
|
+
|
620
|
+
@
|
621
|
+
|
622
|
+
#----------------------------------
|
623
|
+
# The RecentContent list!
|
624
|
+
# Gets filled with ContentMinimal views
|
625
|
+
#
|
626
|
+
# Note that because of Pagination, the list of content is
|
627
|
+
# stored in @resultsEl, not @el
|
628
|
+
class @RecentContent extends @ContentList
|
629
|
+
container: "#aggregator-recent-content"
|
630
|
+
resultsId: "#aggregator-recent-content-results"
|
631
|
+
template: JST[Aggregator.TemplatePath + 'recent_content']
|
632
|
+
|
633
|
+
#---------------------
|
634
|
+
# Need to populate right away for Recent Content
|
635
|
+
initialize: ->
|
636
|
+
super
|
637
|
+
@request()
|
638
|
+
|
639
|
+
#---------------------
|
640
|
+
# Sets up default parameters, and then proxies to #_fetch
|
641
|
+
request: (params={}) ->
|
642
|
+
_.defaults params,
|
643
|
+
limit: @per_page
|
644
|
+
page: 1
|
645
|
+
query: ""
|
646
|
+
|
647
|
+
@_fetch(params)
|
648
|
+
false # To keep consistent with Search#request
|
649
|
+
|
650
|
+
|
651
|
+
#----------------------------------
|
652
|
+
# SEARCH?!?!
|
653
|
+
# This view is the entire Search section. It it made up of
|
654
|
+
# smaller "ContentMinimal" views
|
655
|
+
#
|
656
|
+
# Note that because of the Input field and pagination,
|
657
|
+
# the list of content is actually stored in @resultsEl, not @el
|
658
|
+
#
|
659
|
+
# @render() is for rendering the full section.
|
660
|
+
# Use @renderCollection for rendering just the search results.
|
661
|
+
class @Search extends @ContentList
|
662
|
+
container: "#aggregator-search"
|
663
|
+
resultsId: "#aggregator-search-results"
|
664
|
+
template: JST[Aggregator.TemplatePath + "search"]
|
665
|
+
events:
|
666
|
+
"click .pagination a" : "changePage"
|
667
|
+
"click a.btn" : "search"
|
668
|
+
"keyup input" : "searchIfKeyIsEnter"
|
669
|
+
|
670
|
+
#---------------------
|
671
|
+
# Just a simple proxy to #request to fill in the args properly
|
672
|
+
# Can't make the event delegate straigh to #request because
|
673
|
+
# Backbone automatically passes the event object as the
|
674
|
+
# argument, but #request doesn't handle that.
|
675
|
+
search: (event) ->
|
676
|
+
@_stopEvent(event)
|
677
|
+
@request()
|
678
|
+
false
|
679
|
+
|
680
|
+
#---------------------
|
681
|
+
# Perform a search if the key pressed was the Enter key
|
682
|
+
searchIfKeyIsEnter: (event) ->
|
683
|
+
@search(event) if @_keypressIsEnter(event)
|
684
|
+
|
685
|
+
#---------------------
|
686
|
+
# Sets up default parameters, and then proxies to #_fetch
|
687
|
+
request: (params={}) ->
|
688
|
+
_.defaults params,
|
689
|
+
limit: @per_page
|
690
|
+
page: 1
|
691
|
+
query: $("#aggregator-search-input", @$el).val()
|
692
|
+
|
693
|
+
@_fetch(params)
|
694
|
+
false # to keep the Rails form from submitting
|
695
|
+
|
696
|
+
|
697
|
+
#----------------------------------
|
698
|
+
# The URL Import view
|
699
|
+
# Inherits from @ContentList but doesn't actually
|
700
|
+
# need all of its goodies. That's okay.
|
701
|
+
class @URL extends @ContentList
|
702
|
+
container: "#aggregator-url"
|
703
|
+
resultsId: "#aggregator-url-results"
|
704
|
+
template: JST[Aggregator.TemplatePath + "url"]
|
705
|
+
events:
|
706
|
+
"click a.btn" : "importUrl"
|
707
|
+
"keyup input" : "importUrlIfKeyIsEnter"
|
708
|
+
|
709
|
+
importUrl: (event) ->
|
710
|
+
@_stopEvent(event)
|
711
|
+
@request()
|
712
|
+
false
|
713
|
+
|
714
|
+
#---------------------
|
715
|
+
# Perform a fetch if the key pressed was the Enter key
|
716
|
+
importUrlIfKeyIsEnter: (event) ->
|
717
|
+
@importUrl(event) if @_keypressIsEnter(event)
|
718
|
+
|
719
|
+
#---------------------
|
720
|
+
|
721
|
+
append: (model) ->
|
722
|
+
view = new outpost.Aggregator.Views.ContentMinimal
|
723
|
+
model: model
|
724
|
+
|
725
|
+
@resultsEl.append view.render()
|
726
|
+
@$el
|
727
|
+
|
728
|
+
#---------------------
|
729
|
+
# Proxies to @base.importUrl
|
730
|
+
# Also handles transitions
|
731
|
+
# This overrides the default ContentList#_fetch
|
732
|
+
_fetch: (params={}) ->
|
733
|
+
@transitionStart()
|
734
|
+
|
735
|
+
input = $("#aggregator-url-input", @$el)
|
736
|
+
url = input.val()
|
737
|
+
|
738
|
+
@base.importUrl url,
|
739
|
+
success: (data) =>
|
740
|
+
# Returns null if no record is found
|
741
|
+
# If no data, alert the person
|
742
|
+
# Otherwise, turn it into a ContentMinimal view
|
743
|
+
# for easy dragging, and clear the input
|
744
|
+
if data
|
745
|
+
@collection.add data
|
746
|
+
@append @collection.get(data.id)
|
747
|
+
input.val("") # Empty the URL input
|
748
|
+
else
|
749
|
+
@alertNoResults
|
750
|
+
method: "render"
|
751
|
+
message: "Invalid URL"
|
752
|
+
|
753
|
+
error: (jqXHR) => @alertError(xhr: jqXHR)
|
754
|
+
complete: (jqHXR) => @transitionEnd()
|
755
|
+
|
756
|
+
false # Prevent the Rails form from submitting
|
757
|
+
|
758
|
+
|
759
|
+
#----------------------------------
|
760
|
+
#----------------------------------
|
761
|
+
# An abstract class from which the different
|
762
|
+
# representations of a model should inherit
|
763
|
+
class @ContentView extends Backbone.View
|
764
|
+
tagName: 'li'
|
765
|
+
|
766
|
+
#---------------------
|
767
|
+
|
768
|
+
initialize: ->
|
769
|
+
# Add the model ID to the DOM
|
770
|
+
# We have to do this so that we can share content
|
771
|
+
# between the lists.
|
772
|
+
@$el.attr("data-id", @model.id)
|
773
|
+
@options = _.defaults @options, { template: @template }
|
774
|
+
|
775
|
+
#---------------------
|
776
|
+
|
777
|
+
render: ->
|
778
|
+
@$el.html JST[Aggregator.TemplatePath + "#{@options.template}"](content: @model.toJSON(), opts: @viewOptions)
|
779
|
+
|
780
|
+
#----------------------------------
|
781
|
+
# A single piece of content in the drop zone!
|
782
|
+
# Full with lots of information
|
783
|
+
class @ContentFull extends @ContentView
|
784
|
+
attributes:
|
785
|
+
class: "content-full"
|
786
|
+
template: 'content_full'
|
787
|
+
|
788
|
+
#----------------------------------
|
789
|
+
# A single piece of recent content!
|
790
|
+
# Just the basic info
|
791
|
+
class @ContentMinimal extends @ContentView
|
792
|
+
attributes:
|
793
|
+
class: "content-minimal"
|
794
|
+
template: 'content_small'
|
795
|
+
|
796
|
+
#---------------------
|