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.
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
+ #---------------------