outpost-aggregator 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
#---------------------
|