outpost-aggregator 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/.gitignore +17 -0
  2. data/CHANGELOG.md +16 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +117 -0
  6. data/Rakefile +14 -0
  7. data/config.ru +7 -0
  8. data/lib/assets/javascripts/outpost/aggregator/aggregator.js.coffee +796 -0
  9. data/lib/assets/javascripts/outpost/aggregator/templates/_pagination.jst.eco +12 -0
  10. data/lib/assets/javascripts/outpost/aggregator/templates/base.jst.eco +90 -0
  11. data/lib/assets/javascripts/outpost/aggregator/templates/content_full.jst.eco +19 -0
  12. data/lib/assets/javascripts/outpost/aggregator/templates/content_small.jst.eco +15 -0
  13. data/lib/assets/javascripts/outpost/aggregator/templates/drop_zone.jst.eco +1 -0
  14. data/lib/assets/javascripts/outpost/aggregator/templates/error.jst.eco +4 -0
  15. data/lib/assets/javascripts/outpost/aggregator/templates/recent_content.jst.eco +2 -0
  16. data/lib/assets/javascripts/outpost/aggregator/templates/search.jst.eco +6 -0
  17. data/lib/assets/javascripts/outpost/aggregator/templates/url.jst.eco +5 -0
  18. data/lib/assets/javascripts/outpost/aggregator.js +2 -0
  19. data/lib/assets/stylesheets/outpost/aggregator.css.scss +93 -0
  20. data/lib/outpost/aggregator/json_input.rb +75 -0
  21. data/lib/outpost/aggregator/simple_json.rb +12 -0
  22. data/lib/outpost/aggregator/version.rb +5 -0
  23. data/lib/outpost/aggregator.rb +21 -0
  24. data/outpost-aggregator.gemspec +28 -0
  25. data/spec/factories.rb +2 -0
  26. data/spec/internal/config/database.yml +3 -0
  27. data/spec/internal/config/routes.rb +3 -0
  28. data/spec/internal/db/combustion_test.sqlite +0 -0
  29. data/spec/internal/db/schema.rb +3 -0
  30. data/spec/internal/log/.gitignore +1 -0
  31. data/spec/internal/public/favicon.ico +0 -0
  32. data/spec/lib/outpost/aggregator/json_input_spec.rb +4 -0
  33. data/spec/lib/outpost/aggregator/simple_json_spec.rb +4 -0
  34. data/spec/spec_helper.rb +23 -0
  35. 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
+ #---------------------