Add menu items

Add menu items

error: pathspec 'menus/find-and-replace.cson' did not match any file(s) known to git

--- menus/find-and-replace.cson
+++ menus/find-and-replace.cson
@@ -0,0 +1,15 @@
+'menu': [
+  'label': 'Find'
+  'submenu': [
+    { 'label': 'Find in Buffer', 'command': 'find-and-replace:show'}
+    { 'label': 'Replace in Buffer', 'command': 'find-and-replace:show-replace'}
+    { 'type': 'separator' }
+    { 'label': 'Find in Project', 'command': 'project-find:show'}
+    { 'type': 'separator' }
+    { 'label': 'Find Next', 'command': 'find-and-replace:find-next'}
+    { 'label': 'Find Previous', 'command': 'find-and-replace:find-previous'}
+    { 'label': 'Replace Next', 'command': 'find-and-replace:replace-next'}
+    { 'label': 'Replace All', 'command': 'find-and-replace:replace-all'}
+    { 'type': 'separator' }
+  ]
+]


fix failing content migration test

fix failing content migration test

fixes CNVS-7284

subDayCollection is no longer stored in @model.attributes so doing
@model.set 'subDayCollection', collection doesn't work. Instead its
stored on the actual object so needed to be set as
@model.subDayCollection = collection.

Change-Id: I9828587c9337484abc67bf8c5d452ba034622cdc
Reviewed-on: https://gerrit.instructure.com/22913
Reviewed-by: Bracken Mosbacker <48b846001bca4166dc3fb7711563afc17525921e@instructure.com>
Product-Review: Bracken Mosbacker <48b846001bca4166dc3fb7711563afc17525921e@instructure.com>
QA-Review: Bracken Mosbacker <48b846001bca4166dc3fb7711563afc17525921e@instructure.com>
Tested-by: Bryan Madsen <0a3ba1af216659500dd8f41e29f3979f42452870@instructure.com>

--- spec/coffeescripts/models/ContentMigrationSpec.coffee
+++ spec/coffeescripts/models/ContentMigrationSpec.coffee
@@ -47,7 +47,7 @@

   test 'adds daySubsitution JSON to day_subsitutions namespace if daySubCollection exists', ->
     collection = new DaySubstitutionCollection({bar:'baz'})
-    @model.set('daySubCollection', collection)
+    @model.daySubCollection = collection

     collectionJSON = collection.toJSON()
     json = @model.toJSON()
@@ -58,7 +58,7 @@
     dsOptions = {bar: 'baz'}
     collection = new DaySubstitutionCollection

-    @model.set('daySubCollection', collection)
+    @model.daySubCollection = collection
     @model.set('date_shift_options', {bar: 'baz'})

     json = @model.toJSON()


added file

added file

error: pathspec 'app/coffeescripts/TreeChildItemDemandLinkView.coffee' did not match any file(s) known to git

--- app/coffeescripts/TreeChildItemDemandLinkView.coffee
+++ app/coffeescripts/TreeChildItemDemandLinkView.coffee
@@ -0,0 +1,6 @@
+# The view class for each link child item in the tree view. Each child item
+# is <li> tag with an anchor surrounding the name.
+class window.sirius.TreeChildItemDemandLinkView extends window.sirius.TreeChildItemLinkView
+  showContext: (e) ->
+    @targets[0].get('contextMenu').options.menuItems.push { label: 'Visualize Demand', className: 'context_menu_item', event: ((e) -> alert('VIsualize'))}
+    super


modals default height fixed

modals default height fixed

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/andrewjcasal/koding.git/'


ActivitySideBar: machineTree: disable dblClicks for not Running machines


Components: SidebarSection: Make secondary link empty as default


client: remove activity flux usage

client: remove activity flux usage


Remove empty param list

Remove empty param list

--- src/scrollbar-corner-component.coffee
+++ src/scrollbar-corner-component.coffee
@@ -1,6 +1,6 @@
 module.exports =
 class ScrollbarCornerComponent
-  constructor: () ->
+  constructor: ->
     @domNode = document.createElement('div')
     @domNode.classList.add('scrollbar-corner')


fix (Model) setting authorization shouldn't fail if session is empty

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/AppGyver/supersonic.git/'


Adds Tracker model

Adds Tracker model

error: pathspec 'app/assets/javascripts/backbone/models/tracker.js.coffee' did not match any file(s) known to git

--- app/assets/javascripts/backbone/models/tracker.js.coffee
+++ app/assets/javascripts/backbone/models/tracker.js.coffee
@@ -0,0 +1,9 @@
+class ProjectMonitor.Models.Tracker extends Backbone.Model
+  paramRoot: 'tracker'
+
+  defaults:
+    name: null
+
+class ProjectMonitor.Collections.TrackersCollection extends Backbone.Collection
+  model: ProjectMonitor.Models.Tracker
+  url: '/trackers'


Fixed bug with sprites being offset incorrectly when starting with an action not in their ThangType.

--- app/lib/surface/CocoSprite.coffee
+++ app/lib/surface/CocoSprite.coffee
@@ -285,7 +285,7 @@
   ##################################################
   updateAction: ->
     action = @determineAction()
-    isDifferent = action isnt @currentRootAction
+    isDifferent = action isnt @currentRootAction or action is null
     if not action and @thang?.actionActivated and not @stopLogging
       console.error "action is", action, "for", @thang?.id, "from", @currentRootAction, @thang.action, @thang.getActionName?()
       @stopLogging = true


Workspace: Added launcher class.

Workspace: Added launcher class.

error: pathspec 'client/landing/app/CommonViews/workspace/views/workspacefloatingpanelauncher.coffee' did not match any file(s) known to git

--- client/landing/app/CommonViews/workspace/views/workspacefloatingpanelauncher.coffee
+++ client/landing/app/CommonViews/workspace/views/workspacefloatingpanelauncher.coffee
@@ -0,0 +1,93 @@
+class WorkspaceFloatingPaneLauncher extends KDCustomHTMLView
+
+  constructor: (options = {}, data) ->
+
+    options.cssClass = "workspace-launcher active"
+    options.partial  = "+"
+
+    super options, data
+
+    @sessionKeys      = {}
+    @panel            = @getDelegate()
+    @workspace        = @panel.getDelegate()
+    @container        = new KDView cssClass: "workspace-floating-panes"
+    @keysRef          = @workspace.workspaceRef.child "floatingPaneKeys"
+    @isJoinedASession = @workspace.getOptions().joinedASession
+
+    @panel.addSubView @container
+
+    if @isJoinedASession
+      @keysRef.once "value", (snapshot) =>
+        @sessionKeys = snapshot.val()
+        @createPanes()
+    else
+      @createPanes()
+
+  click: ->
+    @toggleClass "active"
+
+  createPanes: ->
+    panes = @panel.getOptions().floatingPanes
+    panes.forEach (pane) =>
+      @createFloatingPane pane
+      @addSubView new KDCustomHTMLView
+        cssClass : KD.utils.curry "launcher-item", pane
+        tooltip  :
+          title  : pane.capitalize()
+        click    : =>
+          @handleLaunch pane
+
+  createFloatingPane: (paneKey) ->
+    @["create#{paneKey.capitalize()}Pane"]()
+
+  handleLaunch: (paneKey) ->
+    pane = @[paneKey]
+    pane.setClass "active"
+
+    KD.getSingleton("windowController").addLayer pane
+    pane.on 'ReceivedClickElsewhere', =>
+      pane.unsetClass "active"
+
+  createChatPane: ->
+    @container.addSubView @chat = new ChatPane
+      delegate : @panel.getDelegate()
+      floating : yes
+
+  createTerminalPane: ->
+    terminalClass    = SharableTerminalPane
+    terminalClass    = SharableClientTerminalPane  if @isJoinedASession
+
+    @container.addSubView @terminal = new KDView
+      cssClass   : "floating-pane"
+      size       : height : 400
+
+    @terminal.addSubView terminalPane = new terminalClass
+      delegate   : @panel
+      sessionKey : @sessionKeys.terminal
+
+    terminalPane.webterm.on "WebTermConnected", =>
+      @keysRef.child("terminal").set
+        key    : terminalPane.remote.session
+        host   : KD.nick()
+        vmName : KD.getSingleton("vmController").defaultVmName
+
+  createPreviewPane: ->
+    @container.addSubView @preview = new KDView
+      cssClass : "floating-pane floating-preview-pane"
+      size     : height : 400
+      partial  : """
+

+
Type a URL to browse it with your friends.

+          Note that, if you click links inside the page it can't be synced. You need to change the URL.
+

+      """
+
+    previewPane  = new CollaborativePreviewPane
+      delegate   : @panel
+      sessionKey : @sessionKeys.preview
+
+    @workspace.on "WorkspaceSyncedWithRemote", =>
+      @keysRef.child("preview").set previewPane.sessionKey
+
+    @preview.addSubView previewPane
+


[social][graph] added comment

[social][graph] added comment

--- workers/social/lib/social/models/graph/graph.coffee
+++ workers/social/lib/social/models/graph/graph.coffee
@@ -100,6 +100,7 @@
         for group in groups
           checkUserCanReadActivity {client: client, group: group}

+  # we may need to add public group's read permission checking
   removePrivateContent:(client, groupId, contents, callback)->
     if contents.length < 1 then return callback null, contents
     @getSecretGroups client, (err, secretGroups)=>


Added Medication datatype

Added Medication datatype

error: pathspec 'app/assets/javascripts/datatypes/medication.js.coffee' did not match any file(s) known to git

--- app/assets/javascripts/datatypes/medication.js.coffee
+++ app/assets/javascripts/datatypes/medication.js.coffee
@@ -0,0 +1,410 @@
+###
+@namespace scoping into the CQL_QDM namespace
+###
+@CQL_QDM ||= {}
+
+
+###
+Data elements that meet criteria using this datatype should document that the
+medication indicated by the QDM category and its corresponding value set is
+being taken by the patient. Keep in mind that when this datatype is used with
+timing relationships, the criterion is looking for a medication being taken for
+the time frame indicated by the timing relationships.
+###
+class CQL_QDM.MedicationActive
+  constructor: (@entry) ->
+    @_cumulativeMedicationDuration = @entry.cumulativeMedicationDuration
+    @_dose = @entry.dose
+  #  @_frequency = @entry.
+    @_route = @entry.route
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Quantity}
+  ###
+  cumulativeMedicationDuration: ->
+    cql.Quantity(@_cumulativeMedicationDuration['unit'],
+      @_cumulativeMedicationDuration['value'])
+
+  ###
+  @returns {Quantity}
+  ###
+  dose: ->
+    cql.Quantity(@_dose['unit'], @_dose['value'])
+
+  ###
+  @returns {Code}
+  ###
+  route: ->
+    cql.Code(@_route.code, @_route.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())
+
+
+###
+Data elements that meet criteria using this datatype should document that the
+medication indicated by the QDM category and its corresponding value set was
+actually administered to the patient.
+###
+class CQL_QDM.MedicationAdministered
+  constructor: (@entry) ->
+    @_cumulativeMedicationDuration = @entry.cumulativeMedicationDuration
+    @_dose = @entry.dose
+  #  @_frequency = @entry.
+    @_negationRationale = @entry.negationRationale
+    @_reason = @entry.reason
+    @_route = @entry.route
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Quantity}
+  ###
+  cumulativeMedicationDuration: ->
+    cql.Quantity(@_cumulativeMedicationDuration['unit'],
+      @_cumulativeMedicationDuration['value'])
+
+  ###
+  @returns {Quantity}
+  ###
+  dose: ->
+    cql.Quantity(@_dose['unit'], @_dose['value'])
+
+  ###
+  @returns {Code}
+  ###
+  negationRationale: ->
+    cql.Code(@_negationRationale.code, @_negationRationale.code_system)
+
+  ###
+  @returns {Code}
+  ###
+  reason: ->
+    cql.Code(@_reason.code, @_reason.code_system)
+
+  ###
+  @returns {Code}
+  ###
+  route: ->
+    cql.Code(@_route.code, @_route.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())
+
+
+###
+Data elements that meet criteria using this datatype should document an
+unexpected or dangerous reaction to the medication indicated by the QDM
+category and its corresponding value set
+###
+class CQL_QDM.MedicationAdverseEffects
+  constructor: (@entry) ->
+    @_reaction = @entry.reaction
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Code}
+  ###
+  reaction: ->
+    cql.Code(@_reaction.code, @_reaction.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())
+
+
+###
+Data elements that meet criteria using this datatype should document an
+immunologically mediated reaction that exhibits specificity and recurrence on
+re-exposure to the offending medication indicated by the QDM category and
+its corresponding value set.
+###
+class CQL_QDM.MedicationAllergy
+  constructor: (@entry) ->
+    @_reaction = @entry.reaction
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Code}
+  ###
+  reaction: ->
+    cql.Code(@_reaction.code, @_reaction.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())
+
+
+###
+Data elements that meet criteria using this datatype should document that the
+medications indicated by the QDM category and its corresponding value set
+should be taken by or given to the patient after being discharged from
+an inpatient encounter.
+###
+class CQL_QDM.MedicationDischarge
+  constructor: (@entry) ->
+    @_dose = @entry.dose
+  #  @_frequency = @entry.
+    @_negationRationale = @entry.negationRationale
+    @_refills = @entry.refills
+    @_route = @entry.route
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Quantity}
+  ###
+  dose: ->
+    cql.Quantity(@_dose['unit'], @_dose['value'])
+
+  ###
+  @returns {Code}
+  ###
+  negationRationale: ->
+    cql.Code(@_negationRationale.code, @_negationRationale.code_system)
+
+  ###
+  @returns {String}
+  ###
+  refills: ->
+    @_refills
+
+  ###
+  @returns {Code}
+  ###
+  route: ->
+    cql.Code(@_route.code, @_route.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())
+
+
+###
+Data elements that meet criteria using this datatype should document that a
+prescription for the medication indicated by the QDM category and its
+corresponding value set has been dispensed and provided to the patient or
+patient proxy. In the ambulatory setting, medications are primarily taken
+directly by patients and not directly observed. Hence, dispensed is the closest
+health provider documentation of medication compliance. In settings where
+patients attest to taking medications in electronic format (perhaps a Personal
+Health Record), patient attestation of “medication taken” may be available.
+###
+class CQL_QDM.MedicationDispensed
+  constructor: (@entry) ->
+    @_cumulativeMedicationDuration = @entry.cumulativeMedicationDuration
+    @_dose = @entry.dose
+  #  @_frequency = @entry.
+    @_negationRationale = @entry.negationRationale
+    @_refills = @entry.refills
+    @_route = @entry.route
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Quantity}
+  ###
+  cumulativeMedicationDuration: ->
+    cql.Quantity(@_cumulativeMedicationDuration['unit'],
+      @_cumulativeMedicationDuration['value'])
+
+  ###
+  @returns {Quantity}
+  ###
+  dose: ->
+    cql.Quantity(@_dose['unit'], @_dose['value'])
+
+  ###
+  @returns {Code}
+  ###
+  negationRationale: ->
+    cql.Code(@_negationRationale.code, @_negationRationale.code_system)
+
+  ###
+  @returns {String}
+  ###
+  refills: ->
+    @_refills
+
+  ###
+  @returns {Code}
+  ###
+  route: ->
+    cql.Code(@_route.code, @_route.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())
+
+
+###
+Data elements that meet criteria using this datatype should document a reaction
+in specific patients representing a low threshold to the normal pharmacological
+action of the medication indicated by the QDM category and its corresponding
+value set.
+###
+class CQL_QDM.MedicationIntolerance
+  constructor: (@entry) ->
+    @_reaction = @entry.reaction
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Code}
+  ###
+  reaction: ->
+    cql.Code(@_reaction.code, @_reaction.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())
+
+
+###
+Data elements that meet criteria using this datatype should document a request
+to a pharmacy to provide the medication indicated by the QDM category and its
+corresponding value set.
+###
+class CQL_QDM.MedicationOrder
+  constructor: (@entry) ->
+    @_activeDatetime = @entry.active_datetime
+    @_cumulativeMedicationDuration = @entry.cumulativeMedicationDuration
+    @_dose = @entry.dose
+  #  @_frequency = @entry.
+    @_method = @entry.method
+    @_negationRationale = @entry.negationRationale
+    @_reason = @entry.reason
+    @_refills = @entry.refills
+    @_route = @entry.route
+    @_signedDatetime = @entry.signed_datetime
+    @_startDatetime = @entry.start_time
+    @_stopDatetime = @entry.end_time
+
+  ###
+  @returns {Date}
+  ###
+  activeDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_activeDatetime, 'X').toDate())
+
+  ###
+  @returns {Quantity}
+  ###
+  cumulativeMedicationDuration: ->
+    cql.Quantity(@_cumulativeMedicationDuration['unit'],
+      @_cumulativeMedicationDuration['value'])
+
+  ###
+  @returns {Quantity}
+  ###
+  dose: ->
+    cql.Quantity(@_dose['unit'], @_dose['value'])
+
+  ###
+  @returns {Code}
+  ###
+  method: ->
+    cql.Code(@_method.code, @_method.code_system)
+
+  ###
+  @returns {Code}
+  ###
+  negationRationale: ->
+    cql.Code(@_negationRationale.code, @_negationRationale.code_system)
+
+  ###
+  @returns {Code}
+  ###
+  reason: ->
+    cql.Code(@_reason.code, @_reason.code_system)
+
+  ###
+  @returns {String}
+  ###
+  refills: ->
+    @_refills
+
+  ###
+  @returns {Code}
+  ###
+  route: ->
+    cql.Code(@_route.code, @_route.code_system)
+
+  ###
+  @returns {Date}
+  ###
+  signedDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_signedDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  startDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_startDatetime, 'X').toDate())
+
+  ###
+  @returns {Date}
+  ###
+  stopDatetime: ->
+    cql.DateTime.fromDate(moment.utc(@_stopDatetime, 'X').toDate())


WIP: changeUsername

WIP: changeUsername


prevent layouting for view fragments

prevent layouting for view fragments

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/AppGyver/grunt-steroids.git/'


Add jasmine tests to check measure calculation (in progress)

error: pathspec 'test/javascripts/map_editor_reactive_measure_test.coffee' did not match any file(s) known to git

--- test/javascripts/map_editor_reactive_measure_test.coffee
+++ test/javascripts/map_editor_reactive_measure_test.coffee
@@ -0,0 +1,65 @@
+#= require map_editor
+beforeEach ->
+  @map = new L.Map(document.createElement('div')).setView [0, 0], 15
+
+describe 'L.Draw.Polyline.ReactiveMeasure', ->
+  beforeEach ->
+    @geod = GeographicLib.Geodesic.WGS84
+    @drawnItems = new L.FeatureGroup().addTo(@map)
+    @edit = new L.EditToolbar.Edit @map,
+      featureGroup: @drawnItems
+      poly: allowIntersection: false
+      selectedPathOptions: L.EditToolbar::options.edit.selectedPathOptions
+    return
+
+  it 'should draw a polyline', ->
+#    @drawnItems.addLayer @poly
+    @edit.enable()
+    @poly = new L.Draw.Polyline @map
+    @poly.addHooks()
+
+    latlng = L.latLng(44.859585, -0.564652)
+    @poly._markers.push(@poly._createMarker(latlng))
+    @poly._poly.addLatLng(latlng)
+
+    latlng = L.latLng(42, -88)
+    @poly._markers.push(@poly._createMarker(latlng))
+    @poly._poly.addLatLng(latlng)
+
+    latlngs = @poly._poly.getLatLngsAsArray()
+    r = L.GeographicUtil.distance(latlngs[1], latlngs[0])
+
+  # test if a polygon is drawn and if perimeter and area match
+  # TODO it should add polygon to edit featureGroup
+  it 'should draw a polygon', ->
+    # generate a square coordinates
+    square = window.Tools.squareFrom([44.93042508, -0.58050000], 10e3)
+
+    g = new L.GeographicUtil.Polygon(square)
+    console.log g.perimeter().toFixed(3) + " " + g.area().toFixed(1)
+#    console.log @drawnItems.getLayers()
+#    expect(@drawnItems.getLayers().length).toBe 1
+
+#    @drawnItems.addLayer poly
+
+#    @edit.enable()
+    poly = new L.Draw.Polygon @map
+    poly.addHooks()
+
+    for latlng in square
+      latlng = L.latLng latlng
+      poly._markers.push poly._createMarker(latlng)
+      poly._poly.addLatLng latlng
+
+
+    expect(poly.type).toBe('polygon')
+
+    polygon = @geod.Polygon(false)
+    for marker in poly._markers
+      point = marker.getLatLng().toArray()
+      polygon.AddPoint point[0], point[1]
+
+    polygon = polygon.Compute(false, true)
+    expect(polygon.perimeter).toBe(g.perimeter())
+    expect(polygon.area).toBe(g.area())
+


Fixed spacing on unit test

Fixed spacing on unit test

--- test/http_request.spec.coffee
+++ test/http_request.spec.coffee
@@ -31,7 +31,7 @@
       request = new AWS.HttpRequest('http://domain.com', '', 'custom')
       expect(request.headers).to.eql(headers)

-      it 'adds the customUserAgent to the user agent header if provided as array', ->
+    it 'adds the customUserAgent to the user agent header if provided as array', ->
       headers = {}
       headers[agentHeader] = AWS.util.userAgent() + ' custom/1'
       request = new AWS.HttpRequest('http://domain.com', '', ['custom', 1])


Add `rbnf.coffee`

Add `rbnf.coffee`

error: pathspec 'lib/twitter_cldr/js/mustache/numbers/rbnf/rbnf.coffee' did not match any file(s) known to git

--- lib/twitter_cldr/js/mustache/numbers/rbnf/rbnf.coffee
+++ lib/twitter_cldr/js/mustache/numbers/rbnf/rbnf.coffee
@@ -0,0 +1,72 @@
+# Copyright 2012 Twitter, Inc
+# http://www.apache.org/licenses/LICENSE-2.0
+
+class TwitterCldr.RBNF
+  constructor : (@locale) ->
+    @default_spellout_options = {
+      'rule_group' : "SpelloutRules",
+      'rule_set' : "spellout-numbering"
+    }
+    @rule_group_cache = {}
+    @rule_set_name_cache = {}
+    @resource = {} # TODO
+
+  format : (number, options = {}) ->
+    rule_group_name = null
+    rule_set_name = null
+    if (options['rule_group']? and options['rule_set']?)
+      [rule_group_name, rule_set_name] = [default_cardinal_options['rule_group'], default_cardinal_options['rule_set']]
+    else
+      [rule_group_name, rule_set_name] = [options['rule_group'], options['rule_set']]
+
+    if (rule_group = rule_group_by_name(rule_group_name))?
+      if (rule_set = rule_group.rule_set_for(rule_set_name))?
+        if rule_set.is_public
+          TwitterCldr.RBNFRuleFormatter.format(number, rule_set, rule_group, @locale)
+        else
+          throw rule_set_name + " is a private rule set and cannot be used directly."
+
+  group_names = {}
+
+  rule_set_names_for_group : (group_name) ->
+    cache_key = TwitterCldr.Utilities.compute_cache_key(locale, group_name)
+    if rule_set_name_cache[cache_key]?
+      rule_set_name_cache[cache_key]
+
+    result = []
+    if (rule_group = rule_group_by_name(group_name))?
+      for rule_set in rule_group.rule_sets
+        if rule_set.is_public
+          result = result.append(rule_set)
+
+    rule_set_name_cache[cache_key] = result
+
+    rule_set_name_cache[cache_key] || []
+
+  rule_group_by_name : (name) ->
+    cache_key = TwitterCldr.Utilities.compute_cache_key(locale, name)
+
+    if rule_group_cache[cache_key]?
+      rule_group_cache[cache_key]
+
+    group_data = null
+    for group in @resource
+      if group['type'] == name
+        group_data = group
+        break
+
+    if group_data?
+      rule_group_from_resource(group_data)
+
+  rule_set_from_resource : (rule_set_data) ->
+    new TwitterCldr.RuleSet(
+      [new TwitterCldr.Rule(rule['value'], rule['rule'], rule['radix']) for rule in rule_set_data['rules']],
+      rule_set_data['type'],
+      (rule_set_data['access'] || "public")
+    )
+
+  rule_group_from_resource : (group_data) ->
+    new TwitterCldr.RuleGroup(
+      [rule_set_from_resource(rule_set_data) for rule_set_data in group_data['rule_set']],
+      group_data['type']
+    )


can't very well use grunt-release without gruntfile

error: pathspec 'Gruntfile.coffee' did not match any file(s) known to git

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -0,0 +1,3 @@
+module.exports = (grunt) ->
+
+  grunt.loadNpmTasks 'grunt-release'


test: make Travis work

test: make Travis work

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -63,7 +63,11 @@

       less:
         files: ["<%= config.app %>/assets/less/**/*.less"]
-        tasks: ["less:server", "autoprefixer", "csslint"]
+        tasks: [
+          "less:server"
+          "autoprefixer"
+          # "csslint"
+        ]

     less:
       server:
@@ -238,7 +242,7 @@
   # Test task
   grunt.registerTask "test", [
     "build"
-    "csslint"
+    # "csslint"
     "validation"
   ]


code review

code review

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/scotchsoda/metmap.git/'


Collaboration: Only show permission banner if there is a session.

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -1655,7 +1655,7 @@
     @finderPane.makeReadOnly()

     appView.setClass 'read-only'
-    appView.on 'click', @bound 'readOnlyNotifierCallback_'
+    appView.on 'click', @bound 'readOnlyNotifierCallback_'  if @rtm.isReady

   readOnlyNotifierCallback_: -> @showRequestPermissionView()


disable videmsg button on mobile for now

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/VoiSmart/Rocket.Chat.git/'


Added sprite stub

Added sprite stub

error: pathspec 'src/foundation/sprite.coffee' did not match any file(s) known to git

--- src/foundation/sprite.coffee
+++ src/foundation/sprite.coffee
@@ -0,0 +1,5 @@
+do (window = @, Elyssa = @Elyssa or= {}) ->
+
+  class Elyssa.Sprite extends Elyssa.Node
+    constructor: ->
+


debug

debug

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/drsdrs/dooropener.git/'


Added needed functionality to adminconfig

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/Digipalvelutehdas/APIKA.git/'


clio-23619 defer positioning to next tick

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/seanhealy/lib-themisui.git/'


Some more warning removal

Some more warning removal

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/aki77/atomatigit.git/'


chore(karma): run continuous w/ PhantomJS for travis

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/ShellyCoen/hoodie-plugin-angularjs.git/'


activity: start listening for selected message changes

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/andrewjcasal/koding.git/'


teams: fetch team members api call implemented


app: add profile component helper

app: add profile component helper


No ()

No ()

--- app/assets/javascripts/player/vt/ansi_interpreter.js.coffee
+++ app/assets/javascripts/player/vt/ansi_interpreter.js.coffee
@@ -153,7 +153,7 @@
   handleStandardControlSequence: (data, params) ->
     term = data[data.length - 1]

-    numbers = @parseParams(params)
+    numbers = @parseParams params
     n = numbers[0]
     m = numbers[1]

@@ -232,7 +232,7 @@

   handlePrivateControlSequence: (data, params) ->
     action = data[data.length - 1]
-    modes = @parseParams(params)
+    modes = @parseParams params

     for mode in modes
       if mode is 25


Composite fantasies

Composite fantasies

error: pathspec 'app/assets/javascripts/uploadcare/files/composite.js.coffee' did not match any file(s) known to git

--- app/assets/javascripts/uploadcare/files/composite.js.coffee
+++ app/assets/javascripts/uploadcare/files/composite.js.coffee
@@ -0,0 +1,31 @@
+uploadcare.whenReady ->
+  {
+    namespace,
+    jQuery: $,
+    utils,
+    debug
+  } = uploadcare
+
+  namespace 'uploadcare.files', (ns) ->
+    class ns.CompositeFile extends ns.BaseFile
+      constructor: (settings, @__files) ->
+        super
+
+        # Meaning of file fields for file group:
+        #
+        # @fileId           Group ID (uuid~size)
+        # @fileName         null
+        # @fileSize         Cumulative size
+        # @isStored         If all files are stored
+        # @cdnUrl           Group CDN URL
+        # @cdnUrlModifiers  Group CDN URL modifiers
+        # @previewUrl       null
+        # @isImage          If group has any images (and can be previewed)
+        #
+        #
+        # Files are uploaded separately.
+        # Deferreds are composed of group item promises.
+        # Progress notifications are composed of individual ones
+        #
+        # BaseFile needs to be array-like (see: jQuery),
+        # or provide length and iteration (over one file in non-group cases).


Sidebar: do not try to create sidebar just before page is being reloaded

--- client/Main/maincontroller/mainviews/mainview.coffee
+++ client/Main/maincontroller/mainviews/mainview.coffee
@@ -19,17 +19,13 @@
     @bindTransitionEnd()
     @createHeader()

-    if KD.isLoggedInOnLoad
-    then @createSidebar()
-    else mainController.once 'accountChanged.to.loggedIn', @bound 'createSidebar'
+    if KD.isLoggedInOnLoad then @createSidebar()

     @createPanelWrapper()
     @createMainTabView()

     mainController.ready =>
-      if KD.isLoggedInOnLoad
-      then @createAccountArea()
-      else mainController.once 'accountChanged.to.loggedIn', @bound 'createAccountArea'
+      if KD.isLoggedInOnLoad then @createAccountArea()

       @setStickyNotification()
       @emit 'ready'


groups: wip

groups: wip


fixed height problem, added no-hide when finished


make an id on mongoDB

make an id on mongoDB

error: pathspec 'makeanIDwithMongoDB.coffee' did not match any file(s) known to git

--- makeanIDwithMongoDB.coffee
+++ makeanIDwithMongoDB.coffee
@@ -0,0 +1,37 @@
+# createNewCollection.coffee
+#
+# created by Radjiv Carrere
+# 6 octobre 2014
+#
+# License MIT
+#
+
+mongo = require 'mongodb'
+ObjectID = mongo.ObjectID
+
+server = new mongo.Server "127.0.0.1", 27017, {}
+
+client = new mongo.Db 'rainbowZoo', server, {w:1}
+
+# save() updates existing records or inserts new ones as needed
+unicorn = (dbErr, collection) ->
+
+    id = new ObjectID
+
+    console.log "Unable to access database: #{dbErr}" if dbErr
+    collection.save {
+      #this is a new collection
+      "_id": id,
+      "name":"charlie",
+      "birthday": "02/10/2010",
+      "status":"sleepy"
+
+    }, (err, docs) ->
+        console.log "Unable to save record: #{err}" if err
+        client.close()
+
+#put your collection in the database
+client.open (err, database) ->
+    client.collection 'unicorn', unicorn
+
+console.log "new unicorn"


acertando problema com valores

acertando problema com valores

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/AppCivico/votolegal-www.git/'


Initial commit

Initial commit

sankey.coffee, CoffeeScript i.e., transcompiles to JavaScript -> sankey.js
Help: http://coffeescript.org/.
Help: https://github.com/tamc/Sankey

error: pathspec 'source/core/coffee/module/sankey.coffee' did not match any file(s) known to git

--- source/core/coffee/module/sankey.coffee
+++ source/core/coffee/module/sankey.coffee
@@ -0,0 +1,19 @@
+#
+# IICS®
+# Indian Institute of Crime Science.
+# _______________________________________________________________
+# CoffeeScript, http://coffeescript.org/.
+# A javascript library for drawing Sankey diagrams.
+# https://github.com/tamc/Sankey
+# _______________________________________________________________
+# Architecture and Code Handcrafted by Prabhat Kumar.
+# Architectuur en Code handgemaakt door Prabhat Kumar.
+# @author    : Prabhat Kumar [http://prabhatkumar.org/]
+# @copyright : Sequømics Corporation [http://sequomics.com/]
+# _______________________________________________________________
+# @date      : 02-Sept-2016
+# @license   : Apache, version 2.0
+# @require   : Node.js®
+# @require   : NPM
+# @require   : Grunt
+# _______________________________________________________________


Put back old file

Put back old file

error: pathspec 'app/javascript/src/classes/thumbnail_view.coffee' did not match any file(s) known to git

--- app/javascript/src/classes/thumbnail_view.coffee
+++ app/javascript/src/classes/thumbnail_view.coffee
@@ -0,0 +1,1144 @@
+###
+# Handle the thumbnail view, and navigation for the main view.
+#
+# Handle a large number (thousands) of entries cleanly.  Thumbnail nodes are created
+# as needed, and destroyed when they scroll off screen.  This gives us constant
+# startup time, loads thumbnails on demand, allows preloading thumbnails in advance
+# by creating more nodes in advance, and keeps memory usage constant.
+###
+
+window.ThumbnailView = (container, view) ->
+  @container = container
+  @view = view
+  @post_ids = []
+  @post_frames = []
+  @expanded_post_idx = null
+  @centered_post_idx = null
+  @centered_post_offset = 0
+  @last_mouse_x = 0
+  @last_mouse_y = 0
+  @thumb_container_shown = true
+  @allow_wrapping = true
+  @thumb_preload_container = new PreloadContainer
+  @unused_thumb_pool = []
+
+  ### The [first, end) range of posts that are currently inside .post-browser-posts. ###
+
+  @posts_populated = [
+    0
+    0
+  ]
+  document.on 'DOMMouseScroll', @document_mouse_wheel_event.bindAsEventListener(this)
+  document.on 'mousewheel', @document_mouse_wheel_event.bindAsEventListener(this)
+  document.on 'viewer:displayed-image-loaded', @displayed_image_loaded_event.bindAsEventListener(this)
+  document.on 'viewer:set-active-post', ((e) ->
+    post_id_and_frame = [
+      e.memo.post_id
+      e.memo.post_frame
+    ]
+    @set_active_post post_id_and_frame, e.memo.lazy, e.memo.center_thumbs
+    return
+  ).bindAsEventListener(this)
+  document.on 'viewer:show-next-post', ((e) ->
+    @show_next_post e.memo.prev
+    return
+  ).bindAsEventListener(this)
+  document.on 'viewer:scroll', ((e) ->
+    @scroll e.memo.left
+    return
+  ).bindAsEventListener(this)
+  document.on 'viewer:set-thumb-bar', ((e) ->
+    if e.memo.toggle
+      @show_thumb_bar !@thumb_container_shown
+    else
+      @show_thumb_bar e.memo.set
+    return
+  ).bindAsEventListener(this)
+  document.on 'viewer:loaded-posts', @loaded_posts_event.bindAsEventListener(this)
+  @hashchange_post_id = @hashchange_post_id.bind(this)
+  UrlHash.observe 'post-id', @hashchange_post_id
+  UrlHash.observe 'post-frame', @hashchange_post_id
+  new DragElement(@container, ondrag: @container_ondrag.bind(this))
+  Element.on window, 'resize', @window_resize_event.bindAsEventListener(this)
+  @container.on 'mousemove', @container_mousemove_event.bindAsEventListener(this)
+  @container.on 'mouseover', @container_mouseover_event.bindAsEventListener(this)
+  @container.on 'mouseout', @container_mouseout_event.bindAsEventListener(this)
+  @container.on 'click', @container_click_event.bindAsEventListener(this)
+  @container.on 'dblclick', '.post-thumb,.browser-thumb-hover-overlay', @container_dblclick_event.bindAsEventListener(this)
+
+  ### Prevent the default behavior of left-clicking on the expanded thumbnail overlay.  It's
+  # handled by container_click_event.
+  ###
+
+  @container.down('.browser-thumb-hover-overlay').on 'click', ((event) ->
+    if event.isLeftClick()
+      event.preventDefault()
+    return
+  ).bindAsEventListener(this)
+
+  ###
+  # For Android browsers, we're set to 150 DPI, which (in theory) scales us to a consistent UI size
+  # based on the screen DPI.  This means that we can determine the physical screen size from the
+  # window resolution: 150x150 is 1"x1".  Set a thumbnail scale based on this.  On a 320x480 HVGA
+  # phone screen the thumbnails are about 2x too big, so set thumb_scale to 0.5.
+  #
+  # For iOS browsers, there's no way to set the viewport based on the DPI, so it's fixed at 1x.
+  # (Note that on Retina screens the browser lies: even though we request 1x, it's actually at
+  # 0.5x and our screen dimensions work as if we're on the lower-res iPhone screen.  We can mostly
+  # ignore this.)  CSS inches aren't implemented (the DPI is fixed at 96), so that doesn't help us.
+  # Fall back on special-casing individual iOS devices.
+  ###
+
+  @config = {}
+  if navigator.userAgent.indexOf('iPad') != -1
+    @config.thumb_scale = 1.0
+  else if navigator.userAgent.indexOf('iPhone') != -1 or navigator.userAgent.indexOf('iPod') != -1
+    @config.thumb_scale = 0.5
+  else if navigator.userAgent.indexOf('Android') != -1
+
+    ### We may be in landscape or portrait; use out the narrower dimension. ###
+
+    width = Math.min(window.innerWidth, window.innerHeight)
+
+    ### Scale a 320-width screen to 0.5, up to 1.0 for a 640-width screen.  Remember
+    # that this width is already scaled by the DPI of the screen due to target-densityDpi,
+    # so these numbers aren't actually real pixels, and this scales based on the DPI
+    # and size of the screen rather than the pixel count.
+    ###
+
+    @config.thumb_scale = scale(width, 320, 640, 0.5, 1.0)
+    debug 'Unclamped thumb scale: ' + @config.thumb_scale
+
+    ### Clamp to [0.5,1.0]. ###
+
+    @config.thumb_scale = Math.min(@config.thumb_scale, 1.0)
+    @config.thumb_scale = Math.max(@config.thumb_scale, 0.5)
+    debug 'startup, window size: ' + window.innerWidth + 'x' + window.innerHeight
+  else
+
+    ### Unknown device, or not a mobile device. ###
+
+    @config.thumb_scale = 1.0
+  debug 'Thumb scale: ' + @config.thumb_scale
+  @config_changed()
+
+  ### Send the initial viewer:thumb-bar-changed event. ###
+
+  @thumb_container_shown = false
+  @show_thumb_bar true
+  return
+
+ThumbnailView::window_resize_event = (e) ->
+  if e.stopped
+    return
+  if @thumb_container_shown
+    @center_on_post_for_scroll @centered_post_idx
+  return
+
+### Show the given posts.  If extending is true, post_ids are meant to extend a previous
+# search; attempt to continue where we left off.
+###
+
+ThumbnailView::loaded_posts_event = (event) ->
+  post_ids = event.memo.post_ids
+  old_post_ids = @post_ids
+  old_centered_post_idx = @centered_post_idx
+  @remove_all_posts()
+
+  ### Filter blacklisted posts. ###
+
+  post_ids = post_ids.reject(Post.is_blacklisted)
+  @post_ids = []
+  @post_frames = []
+  i = 0
+  while i < post_ids.length
+    post_id = post_ids[i]
+    post = Post.posts.get(post_id)
+    if post.frames.length > 0
+      frame_idx = 0
+      while frame_idx < post.frames.length
+        @post_ids.push post_id
+        @post_frames.push frame_idx
+        ++frame_idx
+    else
+      @post_ids.push post_id
+      @post_frames.push -1
+    ++i
+  @allow_wrapping = !event.memo.can_be_extended_further
+
+  ### Show the results box or "no results".  Do this before updating the results box to make sure
+  # the results box isn't hidden when we update, which will make offsetLeft values inside it zero
+  # and break things.  If the reason we have no posts is because we didn't do a search at all,
+  # don't show no-results.
+  ###
+
+  @container.down('.post-browser-no-results').show event.memo.tags? and @post_ids.length == 0
+  @container.down('.post-browser-posts').show @post_ids.length != 0
+  if event.memo.extending
+
+    ###
+    # We're extending a previous search with more posts.  The new post list we get may
+    # not line up with the old one: the post we're focused on may no longer be in the
+    # search, or may be at a different index.
+    #
+    # Find a nearby post in the new results.  Start searching at the post we're already
+    # centered on.  If that doesn't match, move outwards from there.  Only look forward
+    # a little bit, or we may match a post that was never seen and jump forward too far
+    # in the results.
+    ###
+
+    post_id_search_order = sort_array_by_distance(old_post_ids.slice(0, old_centered_post_idx + 3), old_centered_post_idx)
+    initial_post_id = null
+    i = 0
+    while i < post_id_search_order.length
+      post_id_to_search = post_id_search_order[i]
+      post = Post.posts.get(post_id_to_search)
+      if post?
+        initial_post_id = post.id
+        break
+      ++i
+    debug 'center-on-' + initial_post_id
+
+    ### If we didn't find anything that matched, go back to the start. ###
+
+    if !initial_post_id?
+      @centered_post_offset = 0
+      initial_post_id = new_post_ids[0]
+    center_on_post_idx = @post_ids.indexOf(initial_post_id)
+    @center_on_post_for_scroll center_on_post_idx
+  else
+
+    ###
+    # A new search has completed.
+    #
+    # results_mode can be one of the following:
+    #
+    # "center-on-first"
+    # Don't change the active post.  Center the results on the first result.  This is used
+    # when performing a search by clicking on a tag, where we don't want to center on the
+    # post we're on (since it'll put us at some random spot in the results when the user
+    # probably wants to browse from the beginning), and we don't want to change the displayed
+    # post either.
+    #
+    # "center-on-current"
+    # Don't change the active post.  Center the results on the existing current item,
+    # if possible.  This is used when we want to show a new search without disrupting the
+    # shown post, such as the "child posts" link in post info, and when loading the initial
+    # URL hash when we start up.
+    #
+    # "jump-to-first"
+    # Set the active post to the first result, and center on it.  This is used after making
+    # a search in the tags box.
+    ###
+
+    results_mode = event.memo.load_options.results_mode or 'center-on-current'
+    initial_post_id_and_frame = undefined
+    if results_mode == 'center-on-first' or results_mode == 'jump-to-first'
+      initial_post_id_and_frame = [
+        @post_ids[0]
+        @post_frames[0]
+      ]
+    else
+      initial_post_id_and_frame = @get_current_post_id_and_frame()
+    center_on_post_idx = @get_post_idx(initial_post_id_and_frame)
+    if !center_on_post_idx?
+      center_on_post_idx = 0
+    @centered_post_offset = 0
+    @center_on_post_for_scroll center_on_post_idx
+
+    ### If no post is currently displayed and we just completed a search, set the current post.
+    # This happens when first initializing; we wait for the first search to complete to retrieve
+    # info about the post we're starting on, instead of making a separate query.
+    ###
+
+    if results_mode == 'jump-to-first' or !@view.wanted_post_id?
+      @set_active_post initial_post_id_and_frame, false, false, true
+  if !event.memo.tags?
+
+    ### If tags is null then no search has been done, which means we're on a URL
+    # with a post ID and no search, eg. "/post/browse#12345".  Hide the thumb
+    # bar, so we'll just show the post.
+    ###
+
+    @show_thumb_bar false
+  return
+
+ThumbnailView::container_ondrag = (e) ->
+  @centered_post_offset -= e.dX
+  @center_on_post_for_scroll @centered_post_idx
+  return
+
+ThumbnailView::container_mouseover_event = (event) ->
+  li = $(event.target)
+  if !li.hasClassName('.post-thumb')
+    li = li.up('.post-thumb')
+  if li
+    @expand_post li.post_idx
+  return
+
+ThumbnailView::container_mouseout_event = (event) ->
+
+  ### If the mouse is leaving the hover overlay, hide it. ###
+
+  target = $(event.target)
+  if !target.hasClassName('.browser-thumb-hover-overlay')
+    target = target.up('.browser-thumb-hover-overlay')
+  if target
+    @expand_post null
+  return
+
+ThumbnailView::hashchange_post_id = ->
+  post_id_and_frame = @get_current_post_id_and_frame()
+  if !post_id_and_frame[0]?
+    return
+
+  ### If we're already displaying this post, ignore the hashchange.  Don't center on the
+  # post if this is just a side-effect of clicking a post, rather than the user actually
+  # changing the hash.
+  ###
+
+  post_id = post_id_and_frame[0]
+  post_frame = post_id_and_frame[1]
+  if post_id == @view.displayed_post_id and post_frame == @view.displayed_post_frame
+    #    debug("ignored-hashchange");
+    return
+  new_post_idx = @get_post_idx(post_id_and_frame)
+  @centered_post_offset = 0
+  @center_on_post_for_scroll new_post_idx
+  @set_active_post post_id_and_frame, false, false, true
+  return
+
+### Search for the given post ID and frame in the current search results, and return its
+# index.  If the given post isn't in post_ids, return null.
+###
+
+ThumbnailView::get_post_idx = (post_id_and_frame) ->
+  post_id = post_id_and_frame[0]
+  post_frame = post_id_and_frame[1]
+  post_idx = @post_ids.indexOf(post_id)
+  if post_idx == -1
+    return null
+  if post_frame == -1
+    return post_idx
+
+  ### A post-frame is specified.  Search for a matching post-id and post-frame.  We assume
+  # here that all frames for a post are grouped together in post_ids.
+  ###
+
+  post_frame_idx = post_idx
+  while post_frame_idx < @post_ids.length and @post_ids[post_frame_idx] == post_id
+    if @post_frames[post_frame_idx] == post_frame
+      return post_frame_idx
+    ++post_frame_idx
+
+  ### We found a matching post, but not a matching frame.  Return the post. ###
+
+  post_idx
+
+### Return the post and frame that's currently being displayed in the main view, based
+# on the URL hash.  If no post is displayed and no search results are available,
+# return [null, null].
+###
+
+ThumbnailView::get_current_post_id_and_frame = ->
+  post_id = UrlHash.get('post-id')
+  if !post_id?
+    if @post_ids.length == 0
+      return [
+        null
+        null
+      ]
+    else
+      return [
+        @post_ids[0]
+        @post_frames[0]
+      ]
+  post_id = parseInt(post_id)
+  post_frame = UrlHash.get('post-frame')
+  # If no frame is set, attempt to resolve the post_frame we'll display, if the post data
+  # is already loaded.  Otherwise, post_frame will remain null.
+  if !post_frame?
+    post_frame = @view.get_default_post_frame(post_id)
+  [
+    post_id
+    post_frame
+  ]
+
+### Track the mouse cursor when it's within the container. ###
+
+ThumbnailView::container_mousemove_event = (e) ->
+  x = e.pointerX() - (document.documentElement.scrollLeft)
+  y = e.pointerY() - (document.documentElement.scrollTop)
+  @last_mouse_x = x
+  @last_mouse_y = y
+  return
+
+ThumbnailView::document_mouse_wheel_event = (event) ->
+  event.stop()
+  val = undefined
+  if event.wheelDelta
+    val = event.wheelDelta
+  else if event.detail
+    val = -event.detail
+  if @thumb_container_shown
+    document.fire 'viewer:scroll', left: val >= 0
+  else
+    document.fire 'viewer:show-next-post', prev: val >= 0
+  return
+
+### Set the post that's shown in the view.  The thumbs will be centered on the post
+# if center_thumbs is true.  See BrowserView.prototype.set_post for an explanation
+# of no_hash_change.
+###
+
+ThumbnailView::set_active_post = (post_id_and_frame, lazy, center_thumbs, no_hash_change, replace_history) ->
+
+  ### If no post is specified, do nothing.  This will happen if a search returns
+  # no results.
+  ###
+
+  if !post_id_and_frame[0]?
+    return
+  @view.set_post post_id_and_frame[0], post_id_and_frame[1], lazy, no_hash_change, replace_history
+  if center_thumbs
+    post_idx = @get_post_idx(post_id_and_frame)
+    @centered_post_offset = 0
+    @center_on_post_for_scroll post_idx
+  return
+
+ThumbnailView::set_active_post_idx = (post_idx, lazy, center_thumbs, no_hash_change, replace_history) ->
+  if !post_idx?
+    return
+  post_id = @post_ids[post_idx]
+  post_frame = @post_frames[post_idx]
+  @set_active_post [
+    post_id
+    post_frame
+  ], lazy, center_thumbs, no_hash_change, replace_history
+  return
+
+ThumbnailView::show_next_post = (prev) ->
+  if @post_ids.length == 0
+    return
+  current_idx = @get_post_idx([
+    @view.wanted_post_id
+    @view.wanted_post_frame
+  ])
+
+  ### If the displayed post isn't in the thumbnails and we're changing posts, start
+  # at the beginning.
+  ###
+
+  if !current_idx?
+    current_idx = 0
+  add = if prev then -1 else +1
+  if @post_frames[current_idx] != @view.wanted_post_frame and add == +1
+
+    ###
+    # We didn't find an exact match for the frame we're displaying, which usually means
+    # we viewed a post frame, and then the user changed the view to the main post, and
+    # the main post isn't in the thumbnails.
+    #
+    # It's strange to be on the main post, to hit pgdn, and to end up on the second frame
+    # because the nearest match was the first frame.  Instead, we should end up on the first
+    # frame.  To do that, just don't add anything to the index.
+    ###
+
+    debug 'Snapped the display to the nearest frame'
+    if add == +1
+      add = 0
+  new_idx = current_idx
+  new_idx += add
+  new_idx += @post_ids.length
+  new_idx %= @post_ids.length
+  wrapped = prev and new_idx > current_idx or !prev and new_idx < current_idx
+  if wrapped
+
+    ### Only allow wrapping over the edge if we've already expanded the results. ###
+
+    if !@allow_wrapping
+      return
+    if !@thumb_container_shown and prev
+      notice 'Continued from the end'
+    else if !@thumb_container_shown and !prev
+      notice 'Starting over from the beginning'
+  @set_active_post_idx new_idx, true, true, false, true
+  return
+
+### Scroll the thumbnail view left or right.  Don't change the displayed post. ###
+
+ThumbnailView::scroll = (left) ->
+
+  ### There's no point in scrolling the list if it's not visible. ###
+
+  if !@thumb_container_shown
+    return
+  new_idx = @centered_post_idx
+
+  ### If we're not centered on the post, and we're moving towards the center,
+  # don't jump past the post.
+  ###
+
+  if @centered_post_offset > 0 and left
+    # do nothing
+  else if @centered_post_offset < 0 and !left
+    # do nothing
+  else
+    new_idx += if left then -1 else +1
+  # Snap to the nearest post.
+  @centered_post_offset = 0
+
+  ### Wrap the new index. ###
+
+  if new_idx < 0
+
+    ### Only allow scrolling over the left edge if we've already expanded the results. ###
+
+    if !@allow_wrapping
+      new_idx = 0
+    else
+      new_idx = @post_ids.length - 1
+  else if new_idx >= @post_ids.length
+    if !@allow_wrapping
+      new_idx = @post_ids.length - 1
+    else
+      new_idx = 0
+  @center_on_post_for_scroll new_idx
+  return
+
+### Hide the hovered post, if any, call center_on_post(post_idx), then hover over the correct post again. ###
+
+ThumbnailView::center_on_post_for_scroll = (post_idx) ->
+  if @thumb_container_shown
+    @expand_post null
+  @center_on_post post_idx
+
+  ###
+  # Now that we've re-centered, we need to expand the correct image.  Usually, we can just
+  # wait for the mouseover event to fire, since we hid the expanded thumb overlay and the
+  # image underneith it is now under the mouse.  However, browsers are badly broken here.
+  # Opera doesn't fire mouseover events when the element under the cursor is hidden.  FF
+  # fires the mouseover on hide, but misses the mouseout when the new overlay is shown, so
+  # the next time it's hidden mouseover events are lost.
+  #
+  # Explicitly figure out which item we're hovering over and expand it.
+  ###
+
+  if @thumb_container_shown
+    element = document.elementFromPoint(@last_mouse_x, @last_mouse_y)
+    element = $(element)
+    if element
+      li = element.up('.post-thumb')
+      if li
+        @expand_post li.post_idx
+  return
+
+ThumbnailView::remove_post = (right) ->
+  if @posts_populated[0] == @posts_populated[1]
+    return false
+
+  ### none to remove ###
+
+  node = @container.down('.post-browser-posts')
+  if right
+    --@posts_populated[1]
+    node_to_remove = node.lastChild
+  else
+    ++@posts_populated[0]
+    node_to_remove = node.firstChild
+
+  ### Remove the thumbnail that's no longer visible, and put it in unused_thumb_pool
+  # so we can reuse it later.  This won't grow out of control, since we'll always use
+  # an item from the pool if available rather than creating a new one.
+  ###
+
+  item = node.removeChild(node_to_remove)
+  @unused_thumb_pool.push item
+  true
+
+ThumbnailView::remove_all_posts = ->
+  continue while @remove_post(true)
+
+### Add the next thumbnail to the left or right side. ###
+
+ThumbnailView::add_post_to_display = (right) ->
+  node = @container.down('.post-browser-posts')
+  if right
+    post_idx_to_populate = @posts_populated[1]
+    if post_idx_to_populate == @post_ids.length
+      return false
+    ++@posts_populated[1]
+    thumb = @create_thumb(post_idx_to_populate)
+    node.insertBefore thumb, null
+  else
+    if @posts_populated[0] == 0
+      return false
+    --@posts_populated[0]
+    post_idx_to_populate = @posts_populated[0]
+    thumb = @create_thumb(post_idx_to_populate)
+    node.insertBefore thumb, node.firstChild
+  true
+
+### Fill the container so post_idx is visible. ###
+
+ThumbnailView::populate_post = (post_idx) ->
+  if @is_post_idx_shown(post_idx)
+    return
+
+  ### If post_idx is on the immediate border of what's already displayed, add it incrementally, and
+  # we'll cull extra posts later.  Otherwise, clear all of the posts and populate from scratch.
+  ###
+
+  if post_idx == @posts_populated[1]
+    @add_post_to_display true
+    return
+  else if post_idx == @posts_populated[0]
+    @add_post_to_display false
+    return
+
+  ### post_idx isn't on the boundary, so we're jumping posts rather than scrolling.
+  # Clear the container and start over.
+  ###
+
+  @remove_all_posts()
+  node = @container.down('.post-browser-posts')
+  thumb = @create_thumb(post_idx)
+  node.appendChild thumb
+  @posts_populated[0] = post_idx
+  @posts_populated[1] = post_idx + 1
+  return
+
+ThumbnailView::is_post_idx_shown = (post_idx) ->
+  if post_idx >= @posts_populated[1]
+    return false
+  post_idx >= @posts_populated[0]
+
+### Return the total width of all thumbs to the left or right of post_idx, not
+# including itself.
+###
+
+ThumbnailView::get_width_adjacent_to_post = (post_idx, right) ->
+  post = $('p' + post_idx)
+  if right
+    rightmost_node = post.parentNode.lastChild
+    if rightmost_node == post
+      return 0
+    right_edge = rightmost_node.offsetLeft + rightmost_node.offsetWidth
+    center_post_right_edge = post.offsetLeft + post.offsetWidth
+    right_edge - center_post_right_edge
+  else
+    post.offsetLeft
+
+### Center the thumbnail strip on post_idx.  If post_id isn't in the display, do nothing.
+# Fire viewer:need-more-thumbs if we're scrolling near the edge of the list.
+###
+
+ThumbnailView::center_on_post = (post_idx) ->
+  if !@post_ids
+    debug 'unexpected: center_on_post has no post_ids'
+    return
+  post_id = @post_ids[post_idx]
+  if !Post.posts.get(post_id)?
+    return
+  if post_idx > @post_ids.length * 3 / 4
+
+    ### We're coming near the end of the loaded posts, so load more.  We may be currently
+    # in the middle of setting up the post; defer this, so we finish what we're doing first.
+    ###
+
+    (->
+      document.fire 'viewer:need-more-thumbs', view: this
+      return
+    ).defer()
+  @centered_post_idx = post_idx
+
+  ### If we're not expanded, we can't figure out how to center it since we'll have no width.
+  # Also, don't cause thumbnails to be loaded if we're hidden.  Just set centered_post_idx,
+  # and we'll come back here when we're displayed.
+  ###
+
+  if !@thumb_container_shown
+    return
+
+  ### If centered_post_offset is high enough to put the actual center post somewhere else,
+  # adjust it towards zero and change centered_post_idx.  This keeps centered_post_idx
+  # pointing at the item that's actually centered.
+  ###
+
+  loop
+    post = $('p' + @centered_post_idx)
+    if !post
+      break
+    pos = post.offsetWidth / 2 + @centered_post_offset
+    if pos >= 0 and pos < post.offsetWidth
+      break
+    next_post_idx = @centered_post_idx + (if @centered_post_offset > 0 then +1 else -1)
+    next_post = $('p' + next_post_idx)
+    if !next_post?
+      break
+    current_post_center = post.offsetLeft + post.offsetWidth / 2
+    next_post_center = next_post.offsetLeft + next_post.offsetWidth / 2
+    distance = next_post_center - current_post_center
+    @centered_post_offset -= distance
+    @centered_post_idx = next_post_idx
+    post_idx = @centered_post_idx
+    break
+  @populate_post post_idx
+
+  ### Make sure that we have enough posts populated around the one we're centering
+  # on to fill the display.  If we have too many nodes, remove some.
+  ###
+
+  direction = 0
+  while direction < 2
+    right = ! !direction
+
+    ### We need at least this.container.offsetWidth/2 in each direction.  Load a little more, to
+    # reduce flicker.
+    ###
+
+    minimum_distance = @container.offsetWidth / 2
+    minimum_distance *= 1.25
+    maximum_distance = minimum_distance + 500
+    loop
+      added = false
+      width = @get_width_adjacent_to_post(post_idx, right)
+
+      ### If we're offset to the right then we need more data to the left, and vice versa. ###
+
+      width += @centered_post_offset * (if right then -1 else +1)
+      if width < 0
+        width = 1
+      if width < minimum_distance
+
+        ### We need another post.  Stop if there are no more posts to add. ###
+
+        if !@add_post_to_display(right)
+          break
+        added = false
+      else if width > maximum_distance
+
+        ### We have a lot of posts off-screen.  Remove one. ###
+
+        @remove_post right
+
+        ### Sanity check: we should never add and remove in the same direction.  If this
+        # happens, the distance between minimum_distance and maximum_distance may be less
+        # than the width of a single thumbnail.
+        ###
+
+        if added
+          alert 'error'
+          break
+      else
+        break
+    ++direction
+  @preload_thumbs()
+
+  ### We always center the thumb.  Don't clamp to the edge when we're near the first or last
+  # item, so we always have empty space on the sides for expanded landscape thumbnails to
+  # be visible.
+  ###
+
+  thumb = $('p' + post_idx)
+  center_on_position = @container.offsetWidth / 2
+  shift_pixels_right = center_on_position - (thumb.offsetWidth / 2) - (thumb.offsetLeft)
+  shift_pixels_right -= @centered_post_offset
+  shift_pixels_right = Math.round(shift_pixels_right)
+  node = @container.down('.post-browser-scroller')
+  node.setStyle left: shift_pixels_right + 'px'
+  return
+
+### Preload thumbs on the boundary of what's actually displayed. ###
+
+ThumbnailView::preload_thumbs = ->
+  post_idxs = []
+  i = 0
+  while i < 5
+    preload_post_idx = @posts_populated[0] - i - 1
+    if preload_post_idx >= 0
+      post_idxs.push preload_post_idx
+    preload_post_idx = @posts_populated[1] + i
+    if preload_post_idx < @post_ids.length
+      post_idxs.push preload_post_idx
+    ++i
+
+  ### Remove any preloaded thumbs that are no longer in the preload list. ###
+
+  @thumb_preload_container.get_all().each ((element) ->
+    post_idx = element.post_idx
+    if post_idxs.indexOf(post_idx) != -1
+
+      ### The post is staying loaded.  Clear the value in post_idxs, so we don't load it
+      # again down below.
+      ###
+
+      post_idxs[post_idx] = null
+      return
+
+    ### The post is no longer being preloaded.  Remove the preload. ###
+
+    @thumb_preload_container.cancel_preload element
+    return
+  ).bind(this)
+
+  ### Add new preloads. ###
+
+  i = 0
+  while i < post_idxs.length
+    post_idx = post_idxs[i]
+    if !post_idx?
+      ++i
+      continue
+    post_id = @post_ids[post_idx]
+    post = Post.posts.get(post_id)
+    post_frame = @post_frames[post_idx]
+    url = undefined
+    if post_frame != -1
+      url = post.frames[post_frame].preview_url
+    else
+      url = post.preview_url
+    element = @thumb_preload_container.preload(url)
+    element.post_idx = post_idx
+    ++i
+  return
+
+ThumbnailView::expand_post = (post_idx) ->
+
+  ### Thumbs on click for touchpads doesn't make much sense anyway--touching the thumb causes it
+  # to be loaded.  It also triggers a bug in iPhone WebKit (covering up the original target of
+  # a mouseover during the event seems to cause the subsequent click event to not be delivered).
+  # Just disable hover thumbnails for touchscreens.
+  ###
+
+  if Prototype.BrowserFeatures.Touchscreen
+    return
+  if !@thumb_container_shown
+    return
+  post_id = @post_ids[post_idx]
+  overlay = @container.down('.browser-thumb-hover-overlay')
+  overlay.hide()
+  overlay.down('IMG').src = Vars.asset['blank.gif']
+  @expanded_post_idx = post_idx
+  if !post_idx?
+    return
+  post = Post.posts.get(post_id)
+  if post.status == 'deleted'
+    return
+  thumb = $('p' + post_idx)
+  bottom = @container.down('.browser-bottom-bar').offsetHeight
+  overlay.style.bottom = bottom + 'px'
+  post_frame = @post_frames[post_idx]
+  image_width = undefined
+  image_url = undefined
+  if post_frame != -1
+    frame = post.frames[post_frame]
+    image_width = frame.preview_width
+    image_url = frame.preview_url
+  else
+    image_width = post.actual_preview_width
+    image_url = post.preview_url
+  left = thumb.cumulativeOffset().left - (image_width / 2) + thumb.offsetWidth / 2
+  overlay.style.left = left + 'px'
+
+  ### If the hover thumbnail overflows the right edge of the viewport, it'll extend the document and
+  # allow scrolling to the right, which we don't want.  overflow: hidden doesn't fix this, since this
+  # element is absolutely positioned.  Set the max-width to clip the right side of the thumbnail if
+  # necessary.
+  ###
+
+  max_width = document.viewport.getDimensions().width - left
+  overlay.style.maxWidth = max_width + 'px'
+  overlay.href = '/post/browse#' + post.id + @view.post_frame_hash(post, post_frame)
+  overlay.down('IMG').src = image_url
+  overlay.show()
+  return
+
+ThumbnailView::create_thumb = (post_idx) ->
+  post_id = @post_ids[post_idx]
+  post_frame = @post_frames[post_idx]
+  post = Post.posts.get(post_id)
+
+  ###
+  # Reuse thumbnail blocks that are no longer in use, to avoid WebKit memory leaks: it
+  # doesn't like creating and deleting lots of images (or blocks with images inside them).
+  #
+  # Thumbnails are hidden until they're loaded, so we don't show ugly load-borders.  This
+  # also keeps us from showing old thumbnails before the new image is loaded.  Use visibility:
+  # hidden, not display: none, or the size of the image won't be defined, which breaks
+  # center_on_post.
+  ###
+
+  if @unused_thumb_pool.length == 0
+    div = '<div class="inner">' + '<a class="thumb" tabindex="-1">' + '<img alt="" class="preview" onload="this.style.visibility = \'visible\';">' + '</a>' + '</div>'
+    item = $(document.createElement('li'))
+    item.innerHTML = div
+    item.className = 'post-thumb'
+  else
+    item = @unused_thumb_pool.pop()
+  item.id = 'p' + post_idx
+  item.post_idx = post_idx
+  item.down('A').href = '/post/browse#' + post.id + @view.post_frame_hash(post, post_frame)
+
+  ### If the image is already what we want, then leave it alone.  Setting it to what it's
+  # already set to won't necessarily cause onload to be fired, so it'll never be set
+  # back to visible.
+  ###
+
+  img = item.down('IMG')
+  url = undefined
+  if post_frame != -1
+    url = post.frames[post_frame].preview_url
+  else
+    url = post.preview_url
+  if img.src != url
+    img.style.visibility = 'hidden'
+    img.src = url
+  @set_thumb_dimensions item
+  item
+
+ThumbnailView::set_thumb_dimensions = (li) ->
+  post_idx = li.post_idx
+  post_id = @post_ids[post_idx]
+  post_frame = @post_frames[post_idx]
+  post = Post.posts.get(post_id)
+  width = undefined
+  height = undefined
+  if post_frame != -1
+    frame = post.frames[post_frame]
+    width = frame.preview_width
+    height = frame.preview_height
+  else
+    width = post.actual_preview_width
+    height = post.actual_preview_height
+  width *= @config.thumb_scale
+  height *= @config.thumb_scale
+
+  ### This crops blocks that are too wide, but doesn't pad them if they're too
+  # narrow, since that creates odd spacing.
+  #
+  # If the height of this block is changed, adjust .post-browser-posts-container in
+  # config_changed.
+  ###
+
+  block_size = [
+    Math.min(width, 200 * @config.thumb_scale)
+    200 * @config.thumb_scale
+  ]
+  crop_left = Math.round((width - (block_size[0])) / 2)
+  pad_top = Math.max(0, block_size[1] - height)
+  inner = li.down('.inner')
+  inner.actual_width = block_size[0]
+  inner.actual_height = block_size[1]
+  inner.setStyle
+    width: block_size[0] + 'px'
+    height: block_size[1] + 'px'
+  img = inner.down('img')
+  img.width = width
+  img.height = height
+  img.setStyle
+    marginTop: pad_top + 'px'
+    marginLeft: -crop_left + 'px'
+  return
+
+ThumbnailView::config_changed = ->
+
+  ### Adjust the size of the container to fit the thumbs at the current scale.  They're the
+  # height of the thumb block, plus ten pixels for padding at the top and bottom.
+  ###
+
+  container_height = 200 * @config.thumb_scale + 10
+  @container.down('.post-browser-posts-container').setStyle height: container_height + 'px'
+  @container.select('LI.post-thumb').each @set_thumb_dimensions.bind(this)
+  @center_on_post_for_scroll @centered_post_idx
+  return
+
+### Handle clicks and doubleclicks on thumbnails.  These events are handled by
+# the container, so we don't need to put event handlers on every thumb.
+###
+
+ThumbnailView::container_click_event = (event) ->
+
+  ### Ignore the click if it was stopped by the DragElement. ###
+
+  if event.stopped
+    return
+  if $(event.target).up('.browser-thumb-hover-overlay')
+
+    ### The hover overlay was clicked.  When the user clicks a thumbnail, this is
+    # usually what happens, since the hover overlay covers the actual thumbnail.
+    ###
+
+    @set_active_post_idx @expanded_post_idx
+    event.preventDefault()
+    return
+  li = $(event.target).up('.post-thumb')
+  if !li?
+    return
+
+  ### An actual thumbnail was clicked.  This can happen if we don't have the expanded
+  # thumbnails for some reason.
+  ###
+
+  event.preventDefault()
+  @set_active_post_idx li.post_idx
+  return
+
+ThumbnailView::container_dblclick_event = (event) ->
+  if event.button
+    return
+  event.preventDefault()
+  @show_thumb_bar false
+  return
+
+ThumbnailView::show_thumb_bar = (shown) ->
+  if @thumb_container_shown == shown
+    return
+  @thumb_container_shown = shown
+  @container.show shown
+
+  ### If the centered post was changed while we were hidden, it wasn't applied by
+  # center_on_post, so do it now.
+  ###
+
+  @center_on_post_for_scroll @centered_post_idx
+  document.fire 'viewer:thumb-bar-changed',
+    shown: @thumb_container_shown
+    height: if @thumb_container_shown then @container.offsetHeight else 0
+  return
+
+### Return the next or previous post, wrapping around if necessary. ###
+
+ThumbnailView::get_adjacent_post_idx_wrapped = (post_idx, next) ->
+  post_idx += if next then +1 else -1
+  post_idx = (post_idx + @post_ids.length) % @post_ids.length
+  post_idx
+
+ThumbnailView::displayed_image_loaded_event = (event) ->
+
+  ### If we don't have a loaded search, then we don't have any nearby posts to preload. ###
+
+  if !@post_ids?
+    return
+  post_id = event.memo.post_id
+  post_frame = event.memo.post_frame
+  post_idx = @get_post_idx([
+    post_id
+    post_frame
+  ])
+  if !post_idx?
+    return
+
+  ###
+  # The image in the post we're displaying is finished loading.
+  #
+  # Preload the next and previous posts.  Normally, one or the other of these will
+  # already be in cache.
+  #
+  # Include the current post in the preloads, so if we switch from a frame back to
+  # the main image, the frame itself will still be loaded.
+  ###
+
+  post_ids_to_preload = []
+  post_ids_to_preload.push [
+    @post_ids[post_idx]
+    @post_frames[post_idx]
+  ]
+  adjacent_post_idx = @get_adjacent_post_idx_wrapped(post_idx, true)
+  if adjacent_post_idx?
+    post_ids_to_preload.push [
+      @post_ids[adjacent_post_idx]
+      @post_frames[adjacent_post_idx]
+    ]
+  adjacent_post_idx = @get_adjacent_post_idx_wrapped(post_idx, false)
+  if adjacent_post_idx?
+    post_ids_to_preload.push [
+      @post_ids[adjacent_post_idx]
+      @post_frames[adjacent_post_idx]
+    ]
+  @view.preload post_ids_to_preload
+  return
+
+### This handler handles global keypress bindings, and fires viewer: events. ###
+
+window.InputHandler = ->
+  TrackFocus()
+
+  ###
+  # Keypresses are aggrevating:
+  #
+  # Opera can only stop key events from keypress, not keydown.
+  #
+  # Chrome only sends keydown for non-alpha keys, not keypress.
+  #
+  # In Firefox, keypress's keyCode value for non-alpha keys is always 0.
+  #
+  # Alpha keys can always be detected with keydown.  Don't use keypress; Opera only provides
+  # charCode to that event, and it's affected by the caps state, which we don't want.
+  #
+  # Use OnKey for alpha key bindings.  For other keys, use keypress in Opera and FF and
+  # keydown in other browsers.
+  ###
+
+  keypress_event_name = if window.opera or Prototype.Browser.Gecko then 'keypress' else 'keydown'
+  document.on keypress_event_name, @document_keypress_event.bindAsEventListener(this)
+  return
+
+InputHandler::handle_keypress = (e) ->
+  key = e.charCode
+  if !key
+    key = e.keyCode
+
+  ### Opera ###
+
+  if key == Event.KEY_ESC
+    if document.focusedElement and document.focusedElement.blur and !document.focusedElement.hasClassName('no-blur-on-escape')
+      document.focusedElement.blur()
+      return true
+  target = e.target
+  if target.tagName == 'INPUT' or target.tagName == 'TEXTAREA'
+    return false
+  if key == 63
+    debug 'xxx'
+    document.fire 'viewer:show-help'
+    return true
+  if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey
+    return false
+  grave_keycode = if Prototype.Browser.WebKit then 192 else 96
+  if key == 32
+    document.fire 'viewer:set-thumb-bar', toggle: true
+  else if key == 49
+    document.fire 'viewer:vote', score: 1
+  else if key == 50
+    document.fire 'viewer:vote', score: 2
+  else if key == 51
+    document.fire 'viewer:vote', score: 3
+  else if key == grave_keycode
+    document.fire 'viewer:vote', score: 0
+  else if key == 65 or key == 97
+    document.fire 'viewer:show-next-post', prev: true
+  else if key == 69 or key == 101
+    document.fire 'viewer:edit-post'
+  else if key == 83 or key == 115
+    document.fire 'viewer:show-next-post', prev: false
+  else if key == 70 or key == 102
+    document.fire 'viewer:focus-tag-box'
+  else if key == 86 or key == 118
+    document.fire 'viewer:view-large-toggle'
+  else if key == Event.KEY_PAGEUP
+    document.fire 'viewer:show-next-post', prev: true
+  else if key == Event.KEY_PAGEDOWN
+    document.fire 'viewer:show-next-post', prev: false
+  else if key == Event.KEY_LEFT
+    document.fire 'viewer:scroll', left: true
+  else if key == Event.KEY_RIGHT
+    document.fire 'viewer:scroll', left: false
+  else
+    return false
+  true
+
+InputHandler::document_keypress_event = (e) ->
+  #alert(e.charCode + ", " + e.keyCode);
+  if @handle_keypress(e)
+    e.stop()
+  return


Add jshint task

Add jshint task

error: pathspec 'grunt/jshint.coffee' did not match any file(s) known to git

--- grunt/jshint.coffee
+++ grunt/jshint.coffee
@@ -0,0 +1,9 @@
+module.exports =
+  options:
+    jshintrc: '.jshintrc'
+  assets:
+    src: ['assets/**/*.js']
+  source:
+    src: ['src/**/*.js', '!src/generated/**/*.js']
+  gruntfile:
+    src: ['gruntfile.js']


Repair insert mode.

Repair insert mode.

I broke it here: 77e1ded091062ca2e52264d222482dcd06290a9b

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -402,9 +402,9 @@
       # Remove focus so the user can't just get himself back into insert mode by typing in the same input
       # box.
       event.srcElement.blur()
-      exitInsertMode()
-      DomUtils.suppressEvent event
-      handledKeydownEvents.push event
+    exitInsertMode()
+    DomUtils.suppressEvent event
+    handledKeydownEvents.push event

   else if (findMode)
     if (KeyboardUtils.isEscape(event))


Give focusInput a memory.

Give focusInput a memory.

- Track the most recently-focused input.
- When focusInput is launched (with a count of 1) start with the most
  recently focused input.

This does two things:
- If the user is editing an element then comes back, they end up back
  where they started.
- It gives focusInput a memory.

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -331,6 +331,17 @@
   enterVisualMode: =>
     new VisualMode()

+# Track the most-recently focused input element. This is used by focusInput to decide which input to initially
+# highlight.
+getFocusedElementIndexByRecency = do ->
+  focusedElement = null
+  installListener window, "focus", (event) ->
+    focusedElement = event.target if DomUtils.isEditable event.target
+
+  (elements) ->
+    Math.max 0, elements.indexOf focusedElement
+
+extend window,
   focusInput: (count) ->
     # Focus the first input element on the page, and create overlays to highlight all the input elements, with
     # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element.
@@ -345,7 +356,11 @@

     return if visibleInputs.length == 0

-    selectedInputIndex = Math.min(count - 1, visibleInputs.length - 1)
+    selectedInputIndex =
+      if count == 1
+        getFocusedElementIndexByRecency visibleInputs.map (visibleInput) -> visibleInput.element
+      else
+        Math.min(count, visibleInputs.length) - 1

     hints = for tuple in visibleInputs
       hint = document.createElement("div")


Instead, just restore the selection, for performFindInPlace().

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -11,6 +11,7 @@
 findModeQuery = { rawQuery: "", matchCount: 0 }
 findModeQueryHasResults = false
 findModeAnchorNode = null
+findModeInitialRange = null
 isShowingHelpDialog = false
 keyPort = null
 # Users can disable Vimium on URL patterns via the settings page.  The following two variables
@@ -702,11 +703,8 @@
   settings.set("findModeRawQuery", findModeQuery.rawQuery)

 performFindInPlace = ->
+  findModeRestoreSelection()
   query = if findModeQuery.isRegex then getNextQueryFromRegexMatches(0) else findModeQuery.parsedQuery
-
-  selection = window.getSelection()
-  selection.collapseToStart() if selection.type == "Range"
-
   findModeQueryHasResults = executeFind(query, { caseSensitive: !findModeQuery.ignoreCase })

 # :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'.
@@ -920,7 +918,27 @@
   else
     HUD.show("/" + findModeQuery.rawQuery + " (No Matches)")

+getCurrentRange = ->
+  selection = getSelection()
+  if selection.type == "None"
+    range = document.createRange()
+    range.setStart document.body, 0
+    range.setEnd document.body, 0
+    range
+  else
+    selection.collapseToStart() if selection.type == "Range"
+    range = selection.getRangeAt 0
+
+findModeSaveSelection = ->
+  findModeInitialRange = getCurrentRange()
+
+findModeRestoreSelection = (range = findModeInitialRange) ->
+  selection = getSelection()
+  selection.removeAllRanges()
+  selection.addRange range
+
 window.enterFindMode = ->
+  findModeSaveSelection()
   findModeQuery = { rawQuery: "" }
   findMode = true
   HUD.show("/")


Some events don't have `keyIdentifier`

Some events don't have `keyIdentifier`

This was throwing an error for me on some events.

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -359,8 +359,8 @@

   # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to
   # avoid / being interpreted as ?
-  if (((event.metaKey || event.ctrlKey || event.altKey) && event.keyCode > 31) ||
-      event.keyIdentifier.slice(0, 2) != "U+")
+  if (((event.metaKey || event.ctrlKey || event.altKey) && event.keyCode > 31) || (
+      event.keyIdentifier && event.keyIdentifier.slice(0, 2) != "U+"))
     keyChar = KeyboardUtils.getKeyChar(event)
     # Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition.
     if (keyChar != "")


Create StorageFreak specs

Create StorageFreak specs

error: pathspec 'spec/storagefreak_spec.coffee' did not match any file(s) known to git

--- spec/storagefreak_spec.coffee
+++ spec/storagefreak_spec.coffee
@@ -0,0 +1,620 @@
+cleanup_tests = ->
+  it 'triggers "dbremote_search_reset" event', ->
+    spy = sinon.spy()
+    @instance.on 'dbremote_search_reset', spy
+    @instance.search(@query)
+
+    expect(spy).to.be.calledOnce
+
+  context 'when a timeout is pending', ->
+    it 'clears the timeout', ->
+      spy = sinon.spy(window, 'clearTimeout')
+      timeout_val = @instance.timeout
+      @instance.search(@query)
+
+      expect(spy.args[0][0]).to.equal(timeout_val)
+      spy.restore()
+
+  context 'when an XHR has already been triggered', ->
+    it 'aborts the XHR', ->
+      @clock.tick(@instance.config.XHRTimeout + 100)
+      spy = sinon.spy @instance.XHR_dfd, 'abort'
+      @instance.search(@query)
+
+      expect(spy).to.be.calledOnce
+      spy.restore()
+
+describe 'StorageFreak', ->
+  @timeout(0)
+
+  before (done)->
+    @respondJSON = (json_data)->
+      @requests[0].respond 200, { "Content-Type": "application/json" }, json_data
+
+    [@shops_obj, @makers_obj] = fixture.load('shops.json', 'makers.json')
+    @makers_json = JSON.stringify @makers_obj
+    @shops_json  = JSON.stringify @shops_obj
+
+    @init_options =
+      url : 'dummy_url'
+      name : 'a_select'
+      query : 'get_param_key'
+      app_name : 'test'
+      maxResultsNum: 5
+      XHRTimeout : 650
+      localCacheTimeout : 6000
+      minCharsForRemoteSearch : 5
+      list_of_replacable_chars : [
+        ['ά', 'α'],
+        ['έ', 'ε'],
+        ['ή', 'η'],
+        ['ί', 'ι'],
+        ['ό', 'ο'],
+        ['ύ', 'υ'],
+        ['ώ', 'ω']
+      ]
+
+    require [
+      'storage_freak'
+      'shims/local_storage_shim'
+    ], (StorageFreak, LocalStorageShim)=>
+      @LocalStorageShim = LocalStorageShim
+      @StorageFreak = StorageFreak
+      done()
+
+  beforeEach ->
+    @requests = []
+    @xhr = sinon.useFakeXMLHttpRequest()
+    @xhr.onCreate = (xhr)=> @requests.push xhr
+
+  afterEach ->
+    @xhr.restore()
+    @instance and @instance.cleanup()
+    localStorage and localStorage.clear()
+
+  describe '.constructor', ->
+    it 'returns an instance when called without new', ->
+      @instance = @StorageFreak(@init_options)
+      expect(@instance).to.be.instanceof(@StorageFreak)
+
+    it 'thows if not all required options are given', ->
+      expect(@StorageFreak).to.throw(/option is required/)
+
+    it 'creates @_db_prefix', ->
+      @instance = @StorageFreak(@init_options)
+      expect(@instance._db_prefix).to.equal('selectorablium.test')
+
+    it 'creates @_data_key', ->
+      @instance = @StorageFreak(@init_options)
+      expect(@instance._data_key).to.equal('a_select_data')
+
+    it 'creates @_timestamp_key', ->
+      @instance = @StorageFreak(@init_options)
+      expect(@instance._timestamp_key).to.equal('a_select_timestamp')
+
+    it 'creates @_data', ->
+      @instance = @StorageFreak(@init_options)
+      expect(@instance._data).to.eql({})
+
+    it 'creates @_events', ->
+      @instance = @StorageFreak(@init_options)
+      expect(@instance._events).to.eql({})
+
+    it 'gets @_storage', ->
+      @instance = @StorageFreak(@init_options)
+      expect(@instance._storage).to.be.empty
+
+    context 'when window.localStorage is available', ->
+      beforeEach ->
+        if !window.localStorage
+          @prev = window.localStorage
+          window.localStorage = null
+
+      afterEach ->
+        @prev and window.localStorage = @prev
+
+      it 'references @_storage to localStorage', ->
+        @instance = @StorageFreak(@init_options)
+        expect(@instance._storage).to.equal(window.localStorage)
+
+    xcontext 'when window.localStorage is not available', ->
+      beforeEach ->
+        ## DOES NOT WORK
+        @prev = window.localStorage
+        window.localStorage = null
+
+      afterEach ->
+        window.localStorage = @prev
+
+      it 'references @_storage to localStorageShim', ->
+        @instance = @StorageFreak(@init_options)
+        expect(@instance._storage).to.be.instanceof(@LocalStorageShim)
+
+  describe 'on', ->
+    beforeEach ->
+      @instance = @StorageFreak(@init_options)
+      @event_name = 'event_name'
+      @func = ->
+
+    it 'registers data', ->
+      @instance.on @event_name, @func
+      expect(@instance._events)
+       .to.have.property(@event_name)
+       .that.is.an('array')
+       .with.deep.property('[0]')
+       .to.include
+          callback: @func
+          context: null
+
+    it 'registers third argument as context', ->
+      test_obj = {koko:'lala'}
+      @instance.on @event_name, @func, test_obj
+
+      expect(@instance._events)
+       .to.have.property(@event_name)
+       .that.is.an('array')
+       .with.deep.property('[0]')
+       .to.include
+          callback: @func
+          context: test_obj
+
+    it 'defaults context to null', ->
+      @instance.on @event_name, @func
+
+      expect(@instance._events)
+       .to.have.property(@event_name)
+       .that.is.an('array')
+       .with.deep.property('[0]')
+       .to.include
+          callback: @func
+          context: null
+
+    it 'registers multiple calls with the same event_name', ->
+      @instance.on @event_name, @func
+      @instance.on @event_name, @func
+
+      expect(@instance._events)
+       .to.have.property(@event_name)
+       .that.is.an('array')
+       .to.have.length(2)
+
+  describe 'init', ->
+    beforeEach ->
+      @instance = @StorageFreak(@init_options)
+
+    context 'when storage has no data', ->
+      beforeEach ->
+        @instance._storage.clear()
+
+      it 'returns a Deferred object', ->
+        result = @instance.init()
+        expect(result).to.respondTo('then')
+
+      it 'makes a GET JSON XHR', ->
+        @instance.init()
+        expect(@requests).to.have.length(1)
+
+      context 'before the XHR resolves', ->
+        it 'triggers a "dbcreate_start" event', ->
+          spy = sinon.spy()
+          @instance.on 'dbcreate_start', spy
+          @instance.init()
+          expect(spy).to.be.calledOnce
+
+        it 'does not add new data to storage', ->
+          @instance.init()
+          expect(@instance._get(@instance._data_key))
+            .to.equal(false)
+
+        it 'does not add data_timestamp to storage', ->
+          @instance.init()
+          expect(@instance._get(@instance._timestamp_key))
+            .to.equal(false)
+
+        it 'does not trigger a "dbcreate_end" event', ->
+          spy = sinon.spy()
+          @instance.on 'dbcreate_end', spy
+          @instance.init()
+          expect(spy).to.not.be.called
+
+        it 'does not resolve the returned deferred', ->
+          result = @instance.init()
+          expect(result.state()).to.equal('pending')
+
+      context 'after the XHR resolves', ->
+        beforeEach ->
+          @spy_end = sinon.spy()
+          @instance.on 'dbcreate_end', @spy_end
+
+          @result = @instance.init()
+          return
+
+        it 'adds data to storage', ->
+          @respondJSON(@shops_json)
+
+          expect(@instance._get(@instance._data_key))
+            .to.not.equal(false)
+
+        it 'adds data_timestamp to storage', ->
+          @respondJSON(@shops_json)
+          expect(@instance._get(@instance._timestamp_key))
+            .to.not.equal(false)
+
+        it 'triggers a "dbcreate_end" event', ->
+          @respondJSON(@shops_json)
+          expect(@spy_end).to.be.calledOnce
+
+        it 'resolves the returned deferred', (done)->
+          @respondJSON(@shops_json)
+          @result.then ->
+            expect(true).to.be.true
+            done()
+
+    context 'when storage has data that need refreshing', ->
+      beforeEach ->
+        @timestamp = new Date().getTime()
+        @timestamp = @timestamp - (2 * @instance.config.localCacheTimeout)
+
+        @instance._set @instance._data_key, @makers_obj
+        @old_data = @instance._get @instance._data_key
+        @instance._set @instance._timestamp_key, @timestamp
+
+      it 'returns a Deferred object', ->
+        result = @instance.init()
+        expect(result).to.respondTo('then')
+
+      it 'makes a GET JSON XHR', ->
+        @instance.init()
+        expect(@requests).to.have.length(1)
+
+      context 'before the XHR resolves', ->
+        it 'triggers a "dbcreate_start" event', ->
+          spy = sinon.spy()
+          @instance.on 'dbcreate_start', spy
+          @instance.init()
+          expect(spy).to.be.calledOnce
+
+        it 'does not add new data to storage', ->
+          @instance.init()
+          expect(@instance._get(@instance._data_key))
+            .to.eql(@old_data)
+
+        it 'does not add data_timestamp to storage', ->
+          @instance.init()
+          expect(@instance._get(@instance._timestamp_key))
+            .to.eql(@timestamp)
+
+        it 'does not trigger a "dbcreate_end" event', ->
+          spy = sinon.spy()
+          @instance.on 'dbcreate_end', spy
+          @instance.init()
+          expect(spy).to.not.be.called
+
+        it 'does not resolve the returned deferred', ->
+          result = @instance.init()
+          expect(result.state()).to.equal('pending')
+
+      context 'after the XHR resolves', ->
+        beforeEach ->
+          @spy_end = sinon.spy()
+          @instance.on 'dbcreate_end', @spy_end
+
+          @result = @instance.init()
+          return
+
+        it 'adds old data in storage with new', ->
+          @respondJSON(@shops_json)
+
+          expect(@instance._get(@instance._data_key))
+            .to.not.eql(@old_data)
+
+        it 'replaces old data_timestamp in storage with new', ->
+          @respondJSON(@shops_json)
+          expect(@instance._get(@instance._timestamp_key))
+            .to.not.eql(@timestamp)
+
+        it 'triggers a "dbcreate_end" event', ->
+          @respondJSON(@shops_json)
+          expect(@spy_end).to.be.calledOnce
+
+        it 'resolves the returned deferred', (done)->
+          @respondJSON(@shops_json)
+          @result.then ->
+            expect(true).to.be.true
+            done()
+
+    context 'when storage has data', ->
+      beforeEach ->
+        @instance._updateDB(@shops_obj)
+        @ret = @instance.init()
+
+      it 'returns a Deferred object', ->
+        expect(@ret).to.respondTo('then')
+
+      it 'does not make an XHR', ->
+        expect(@requests).to.have.length(0)
+
+      it 'retrieves data from storage', ->
+        expect(@instance._data).eql(@shops_obj)
+
+      it 'resolves returned deferred', (done)->
+        @ret.then ->
+          expect(true).to.be.true
+          done()
+
+  describe 'searchByKey', ->
+    beforeEach ->
+      @instance = @StorageFreak(@init_options)
+      @instance._data = {
+        koko: 'lala'
+      }
+
+    it 'returns value for searched key', ->
+      expect(@instance.searchByKey('koko')).to.equal('lala')
+
+    it 'returns false if nothing is found', ->
+      expect(@instance.searchByKey('lala')).to.equal(false)
+
+    it 'does not make an XHR', ->
+      expect(@requests).to.have.length(0)
+
+  describe 'search', ->
+    beforeEach ->
+      @instance = @StorageFreak(@init_options)
+
+    it 'does not search storage', ->
+      spy = sinon.spy @instance._storage, 'getItem'
+
+      @instance.search('as')
+      expect(spy).to.not.be.called
+
+    it 'searches in-memory data', (done)->
+      @instance._data = {
+        add: 'add'
+        asd: 'asd'
+        as: 'as'
+      }
+
+      @instance.on 'dbsearch_results', (data, query, remote = false)->
+        expect(data)
+          .to.be.an('array')
+          .to.have.length(2)
+        done()
+
+      @instance.search('as')
+
+    xit 'matches accented characters ignoring their accent', ->
+
+    it 'triggers "dbsearch_results" event and passes results', (done)->
+      @instance._data = {
+        as: 'as'
+        asd: 'asd'
+        add: 'add'
+      }
+
+      @instance.on 'dbsearch_results', (data, query)->
+        expect(data)
+          .to.be.an('array')
+          .to.have.length(2)
+        done()
+
+      @instance.search('as')
+
+    it 'also passes original query as second argument to the event callback', (done)->
+      @instance._data = {
+        as: 'as'
+        asd: 'asd'
+        add: 'add'
+      }
+
+      @instance.on 'dbsearch_results', (data, query)->
+        expect(query).to.equal('as')
+        done()
+
+      @instance.search('as')
+
+    it 'sorts results', (done)->
+      @instance._data =
+        asd: 'asd'
+        add: 'add'
+        as: 'as'
+
+      @instance.on 'dbsearch_results', (data)->
+        expect(data).to.deep.equal [
+          { id: 'as', name: 'as' },
+          { id: 'asd', name: 'asd' }
+        ]
+        done()
+
+      @instance.search('as')
+
+    it 'slices results', (done)->
+      @instance._data =
+        asd: 'asd'
+        add: 'add'
+        as: 'as'
+        asd1: 'asd1'
+        add1: 'add1'
+        as1: 'as1'
+        asd2: 'asd2'
+        add2: 'add2'
+        as3: 'as2'
+
+      @instance.on 'dbsearch_results', (data)=>
+        expect(data).to.have.length @instance.config.maxResultsNum
+        done()
+
+      @instance.search('as')
+
+    it 'triggers "dbremote_search_reset" event', (done)->
+      @instance.on 'dbremote_search_reset', ->
+        expect(true).to.be.true
+        done()
+      @instance.search('as')
+
+    context 'when query is smaller than @config.minCharsForRemoteSearch', ->
+      it 'does not make an XHR', ->
+        @instance.search('asdasd')
+        expect(@requests).to.have.length(0)
+
+    context 'when query is bigger or equal to @config.minCharsForRemoteSearch', ->
+      beforeEach ->
+        @query = 'asdasd'
+        @instance._data =
+          asdasd0: 'asdasd0'
+
+        @clock = sinon.useFakeTimers()
+
+      afterEach ->
+        @clock.restore()
+
+      it 'registers XHR to occur after timeout', ->
+        spy = sinon.spy(window, 'setTimeout')
+        @instance.search(@query)
+        expect(spy.args[0][1]).to.equal(@instance.config.XHRTimeout)
+        spy.restore()
+
+      context 'before timeout', ->
+        it 'triggers "dbremote_search_in" event and passes @config.XHRTimeout', (done)->
+          @instance.on 'dbremote_search_in', (timeout)=>
+            expect(timeout).to.equal(@instance.config.XHRTimeout)
+            done()
+          @instance.search(@query)
+
+      context 'after timeout', ->
+        beforeEach ->
+          @expected_makers_data = [
+            {
+              'id': 'asdasd0',
+              'name': 'asdasd0'
+            },
+            {
+              'id': 'asdasd3',
+              'name': 'asdasd3'
+            },
+            {
+              'id': 'asdasd4',
+              'name': 'asdasd4'
+            },
+            {
+              'id': 'asdasd5',
+              'name': 'asdasd5'
+            },
+            {
+              'id': 'asdasd6',
+              'name': 'asdasd6'
+            }
+          ]
+
+        it 'makes an XHR', (done)->
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(@requests).to.have.length(1)
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@shops_json)
+
+        it 'updates storage', (done)->
+          @instance._set @instance._data_key, @makers_obj
+          old_data = @instance._get @instance._data_key
+
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(@instance._get(@instance._data_key)).to.not.eql(old_data)
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@makers_json)
+
+        it 'updates @_data', (done)->
+          data = {koko:'lala'}
+          @instance._data = data
+
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(@instance._data).to.not.eql(data)
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@makers_json)
+
+        it 'triggers "dbsearch_results" event and passes results from updated data as first arg', (done)->
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(results).to.deep.eql @expected_makers_data
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@makers_json)
+
+        it 'triggers "dbsearch_results" event and passes original query as second arg', (done)->
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(query).to.equal(@query)
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@makers_json)
+
+        it 'triggers "dbsearch_results" event and passes "xhr" as third arg', (done)->
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(xhr).to.equal('xhr')
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@makers_json)
+
+        it 'sorts results', (done)->
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(results).to.deep.eql @expected_makers_data
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@makers_json)
+
+        it 'slices results', (done)->
+          @instance.on 'dbsearch_results', (results, query, xhr = false)=>
+            if xhr
+              expect(results).to.be.an('array').with.length(@instance.config.maxResultsNum)
+              done()
+
+          @instance.search(@query)
+          @clock.tick(@instance.config.XHRTimeout)
+          @respondJSON(@makers_json)
+
+    context 'when a new remote request starts before the previous finishes', ->
+      beforeEach ->
+        @query = 'asdasd'
+        @instance._data =
+          asdasd0: 'asdasd0'
+        @clock = sinon.useFakeTimers()
+        @instance.search(@query)
+
+      afterEach ->
+        @clock.restore()
+
+      cleanup_tests()
+
+  describe 'cleanup', ->
+    beforeEach ->
+      @instance = @StorageFreak(@init_options)
+      @query = 'asdasd'
+      @instance._data =
+        asdasd0: 'asdasd0'
+      @clock = sinon.useFakeTimers()
+      @instance.search(@query)
+
+    afterEach ->
+      @clock.restore()
+
+    cleanup_tests()


fix: [7e6f0173332fa9a3e8181a1cfc95e0a35d511a2b]

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/netguru/people.git/'