Move animations, tapHandlers, and orientation var directly onto prototype. Define prototype methods in constructor — Gross, but this way jQT maintains the same private/public object as it's had to date (open to changing to nix private stuff…)

Also: Nixed some iOS5 detection stuff. Will be the new minimum.

--- src/jqt.coffee
+++ src/jqt.coffee
@@ -1,99 +1,54 @@
 class $.jQT
   extensions: []
-  animations: []
-  history: []
-  settings: {}
+  animations: [
+    name: "cubeleft"
+    selector: ".cubeleft, .cube"
+    is3d: true
+  ,
+    name: "cuberight"
+    selector: ".cuberight"
+    is3d: true
+  ,
+    name: "dissolve"
+    selector: ".dissolve"
+  ,
+    name: "fade"
+    selector: ".fade"
+  ,
+    name: "flipleft"
+    selector: ".flipleft, .flip"
+    is3d: true
+  ,
+    name: "flipright"
+    selector: ".flipright"
+    is3d: true
+  ,
+    name: "pop"
+    selector: ".pop"
+    is3d: true
+  ,
+    name: "swapleft"
+    selector: ".swapleft, .swap"
+    is3d: true
+  ,
+    name: "swapright"
+    selector: ".swapright"
+    is3d: true
+  ,
+    name: "slidedown"
+    selector: ".slidedown"
+  ,
+    name: "slideright"
+    selector: ".slideright"
+  ,
+    name: "slideup"
+    selector: ".slideup"
+  ,
+    name: "slideleft"
+    selector: ".slideleft, .slide, #jqt > * > ul li a"
+  ]
   orientation: 'portrait'
-
-  addAnimation: ->
-    @::animations.push animation if typeof (animation.selector) is "string" and typeof (animation.name) is "string"
-  addTapHandler: ->
-    tapHandlers.push tapHandler  if typeof (tapHandler.name) is "string" and typeof (tapHandler.isSupported) is "function" and typeof (tapHandler.fn) is "function"
-  getOrientation: ->
-  addTapHandler: (extension) ->
-    @::tapHandlers.push extension
-  goBack: ->
-    # Error checking
-    console.warn "History is empty." if history.length < 1
-    if history.length is 1
-      console.warn "You are on the first panel."
-      window.history.go -1
-    from = history[0]
-    to = history[1]
-    if doNavigation(from.page, to.page, from.animation, true)
-      publicObj
-    else
-      console.warn "Could not go back."
-      false
-
-  insertPages: (nodes, animation) ->
-    # Call dom.createElement element directly
-    # instead of relying on $(nodes), to work around:
-    # https://github.com/madrobby/zepto/issues/312
-
-    # DK: This is allegedly fixed...
-    # div = document.createElement("div")
-    # div.innerHTML = nodes
-
-    $(nodes).children().each (index, node) ->
-      $node = $(this)
-      $node.attr "id", "page-" + (++newPageCount) unless $node.attr("id")
-
-      # Remove any existing instances
-      $("#" + $node.attr("id")).remove()
-
-      $body.append $node
-      $body.trigger "pageInserted",
-        page: $node
-
-      targetPage = $node if $node.hasClass("current") or not targetPage
-
-    if targetPage?
-      goTo targetPage, animation
-      targetPage
-    else
-      false
-
-  goTo: (toPage, animation) ->
-    fromPage = history[0].page
-
-    if typeof animation is "string"
-      for i in animations
-        if animations[i].name is animation
-          animation = animations[i]
-          break
-
-    if typeof toPage is "string"
-      nextPage = $(toPage)
-      unless nextPage.length
-        showPageByHref toPage,
-          animation: animation
-
-        return
-      else
-        toPage = nextPage
-    if doNavigation(fromPage, toPage, animation)
-      publicObj
-    else
-      console.warn "Could not animate pages."
-      false
-
-  # PRIVATE DATA
-
-  $body = undefined
-  $head = $("head")
-  history = []
-  newPageCount = 0
-  jQTSettings = {}
-  $currentPage = ""
-  touchSelectors = []
-  publicObj = {}
-  tapBuffer = 100
-  extensions = $.jQT::extensions
-  extTapHandlers = $.jQT::tapHandlers
-  tapHandlers = []
-  animations = []
-  defaults =
+  defaults:
     addGlossToIcon: true
     backSelector: ".back, .cancel, .goback"
     cacheGetRequests: true
@@ -116,95 +71,52 @@
     useAnimations: true
     useFastTouch: true
     useTouchScroll: true
-    animations: [
-      name: "cubeleft"
-      selector: ".cubeleft, .cube"
-      is3d: true
-    ,
-      name: "cuberight"
-      selector: ".cuberight"
-      is3d: true
-    ,
-      name: "dissolve"
-      selector: ".dissolve"
-    ,
-      name: "fade"
-      selector: ".fade"
-    ,
-      name: "flipleft"
-      selector: ".flipleft, .flip"
-      is3d: true
-    ,
-      name: "flipright"
-      selector: ".flipright"
-      is3d: true
-    ,
-      name: "pop"
-      selector: ".pop"
-      is3d: true
-    ,
-      name: "swapleft"
-      selector: ".swapleft, .swap"
-      is3d: true
-    ,
-      name: "swapright"
-      selector: ".swapright"
-      is3d: true
-    ,
-      name: "slidedown"
-      selector: ".slidedown"
-    ,
-      name: "slideright"
-      selector: ".slideright"
-    ,
-      name: "slideup"
-      selector: ".slideup"
-    ,
-      name: "slideleft"
-      selector: ".slideleft, .slide, #jqt > * > ul li a"
-    ]
-
-  # Private functions
-
-  addPageToHistory = (page, animation) ->
-    id = page.attr 'id'
-
-    history.unshift
-      page: page
-      animation: animation
-      hash: "#" + id
-      id: id
-
-  addDefaultTapHandlers = ->
-    addTapHandler
+
+  # PRIVATE DATA
+
+  $body = undefined
+  $head = $("head")
+  animations = @::animations
+  customHistory = []
+  newPageCount = 0
+  $currentPage = ""
+  touchSelectors = []
+  tapBuffer = 100
+
+  constructor: (options) ->
+
+    # Public methods
+    @addAnimation = ->
+      @animations.push animation if typeof (animation.selector) is "string" and typeof (animation.name) is "string"
+
+    @addTapHandler = (tapHandler) =>
+      @tapHandlers.push tapHandler  if typeof (tapHandler.name) is "string" and typeof (tapHandler.isSupported) is "function" and typeof (tapHandler.fn) is "function"
+
+    @addExtension = (extension) ->
+      @extensions.push extension
+
+
+    @tapHandlers = [
       name: "external-link"
       isSupported: (e, params) -> isExternalLink params.$el
-
       fn: (e, params) ->
         params.$el.removeClass 'active'
         true
-
-    addTapHandler
+    ,
       name: "back-selector"
-      isSupported: (e, params) ->
-        params.$el.is params.jQTSettings.backSelector
-
+      isSupported: (e, params) =>
+        params.$el.is @settings.backSelector
+      fn: (e, params) =>
+        # User clicked or tapped a back button
+        @goBack params.hash
+    ,
+      name: "submit-selector"
+      isSupported: (e, params) =>
+        params.$el.is @settings.submitSelector
       fn: (e, params) ->
-
-        # User clicked or tapped a back button
-        goBack params.hash
-
-    addTapHandler
-      name: "submit-selector"
-      isSupported: (e, params) ->
-        params.$el.is params.jQTSettings.submitSelector
-
-      fn: (e, params) ->
-
         # User clicked or tapped a submit element
         submitParentForm params.$el
-
-    addTapHandler
+    ,
       name: "webapp"
       isSupported: (e, params) ->
         params.target is "_webapp"
@@ -214,36 +126,30 @@
         # User clicked or tapped an internal link, fullscreen mode
         window.location = params.href
         false
-
-    addTapHandler
+    ,
       name: "no-op"
       isSupported: (e, params) ->
         params.href is "#"

       fn: (e, params) ->
-
         # Allow tap on item with no href
         params.$el.removeClass 'active'
         true
-
-    addTapHandler
+    ,
       name: "standard"
       isSupported: (e, params) ->
         params.hash and params.hash isnt "#"

-      fn: (e, params) ->
+      fn: (e, params) =>
         animation = getAnimation(params.$el)

         # Internal href
         params.$el.addClass "active"
-        goTo $(params.hash).data("referrer", params.$el), animation, params.$el.hasClass("reverse")
+        @goTo $(params.hash).data("referrer", params.$el), animation, params.$el.hasClass("reverse")
         false
-
-    addTapHandler
+    ,
       name: "external"
-      isSupported: (e, params) ->
-        true
-
+      isSupported: (e, params) -> true
       fn: (e, params) ->
         animation = getAnimation(params.$el)

@@ -260,419 +166,499 @@
           $referrer: params.$el

         false
-
-  # Unfortunately, we can not assume the 'tap' event
-  # is being used for links, forms, etc.
-  clickHandler = (e) ->
-    $el = $(e.target)
+    ]
+
+    @goTo = (toPage, animation) =>
+      fromPage = customHistory[0].page
+
+      if typeof animation is "string"
+        for anim in animations
+          if anim.name is animation
+            animation = anim
+            break
+
+      if typeof toPage is "string"
+        nextPage = $(toPage)
+        unless nextPage.length
+          showPageByHref toPage,
+            animation: animation
+
+          return
+        else
+          toPage = nextPage
+      if doNavigation(fromPage, toPage, animation)
+        @
+      else
+        console.warn "Could not animate pages."
+        false
+
+    @goBack = ->
+      # Error checking
+      console.warn "History is empty." if customHistory.length < 1
+      if customHistory.length is 1
+        console.warn "You are on the first panel."
+        window.customHistory.go -1
+      from = customHistory[0]
+      to = customHistory[1]
+      if doNavigation(from.page, to.page, from.animation, true)
+        @
+      else
+        console.warn "Could not go back."
+        false
+
+    # PRIVATE METHODS
+
+    initHairExtensions = (options) =>
+
+      # Preload images
+      for i in @settings.preloadImages
+        (new Image()).src = @settings.preloadImages[i]
+
+      hairExtensions = []
+
+      # Set appropriate icon
+      # (retina display available in iOS 4.2 and later.)
+      precomposed = if @settings.addGlossToIcon then '' else '-precomposed'
+      hairExtensions.push """<link rel="apple-touch-icon#{precomposed}" href="#{@settings.icon}">""" if @settings.icon
+      hairExtensions.push """<link rel="apple-touch-icon#{precomposed}" sizes="114x114" href="#{@settings.icon4}">"""  if @settings.icon4
+
+      # Set startup screen
+      hairExtensions.push """<link rel="apple-touch-startup-image" href="#{@settings.startupScreen}">""" if @settings.startupScreen
+
+      # Set viewport
+      hairExtensions.push """<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=0">""" if @settings.fixedViewport
+
+      # Set full-screen
+      if @settings.fullScreen
+        hairExtensions.push """<meta name="apple-mobile-web-app-capable" content="yes" />"""
+        hairExtensions.push """<meta name="apple-mobile-web-app-status-bar-style" content="#{@settings.statusBar}">""" if @settings.statusBar
+
+      # Attach hair extensions
+      $head.prepend hairExtensions.join '' if hairExtensions.length
+
+
+
+
+    insertPages = (nodes, animation) ->
+      # Call dom.createElement element directly
+      # instead of relying on $(nodes), to work around:
+      # https://github.com/madrobby/zepto/issues/312
+
+      # DK: This is allegedly fixed...
+      # div = document.createElement("div")
+      # div.innerHTML = nodes
+
+      $(nodes).children().each (index, node) ->
+        $node = $(this)
+        $node.attr "id", "page-" + (++newPageCount) unless $node.attr("id")
+
+        # Remove any existing instances
+        $("#" + $node.attr("id")).remove()
+
+        $body.append $node
+        $body.trigger "pageInserted",
+          page: $node
+
+        targetPage = $node if $node.hasClass("current") or not targetPage
+
+      if targetPage?
+        @goTo targetPage, animation
+        targetPage
+      else
+        false
+
+    # Private functions
+
+    addPageToHistory = (page, animation) ->
+      id = page.attr 'id'
+
+      customHistory.unshift
+        page: page
+        animation: animation
+        hash: "#" + id
+        id: id
+
+    # Unfortunately, we can not assume the 'tap' event
+    # is being used for links, forms, etc.
+    clickHandler = (e) =>
+      $el = $(e.target)
+
+      # Find the nearest tappable ancestor
+      $el = $el.closest(touchSelectors.join(", ")) unless $el.is(touchSelectors.join(", "))
+
+      # Prevent default if we found an internal link
+      # (relative or absolute)
+      if $el and $el.attr("href") and not isExternalLink($el)
+        console.warn "Need to prevent default click behavior."
+        e.preventDefault()
+      else
+        console.warn "No need to prevent default click behavior."
+
+      # Trigger a tap event if touchstart is not on the job
+      unless $.support.touch
+        console.warn "Converting click event to a tap event because touch handlers are not present or off."
+        $(e.target).trigger "tap", e
+
+    # Fires when the history state changes
+    hashChangeHandler = (e) =>
+      if location.hash is customHistory[0].hash
+        console.warn "We are on the right panel."
+        true
+      else if location.hash is ""
+        goBack()
+        true
+      else if customHistory[1] and location.hash is customHistory[1].hash
+        goBack()
+        true
+      else
+        # Lastly, just try going to the ID...
+        console.warn "Could not find ID in history, just forwarding to DOM element."
+        @goTo $(location.hash), @settings.defaultAnimation

-    # Find the nearest tappable ancestor
-    $el = $el.closest(touchSelectors.join(", ")) unless $el.is(touchSelectors.join(", "))
+    isExternalLink = ($el) ->
+      $el.attr("target") is "_blank" or $el.attr("rel") is "external" or $el.is("a[href^=\"http://maps.google.com\"], a[href^=\"mailto:\"], a[href^=\"tel:\"], a[href^=\"javascript:\"], a[href*=\"youtube.com/v\"], a[href*=\"youtube.com/watch\"]")

-    # Prevent default if we found an internal link
-    # (relative or absolute)
-    if $el and $el.attr("href") and not isExternalLink($el)
-      console.warn "Need to prevent default click behavior."
+    getAnimation = ($el) =>
+      for animation in animations
+        if $el.is(animation.selector)
+          resultAnimation = animation
+          break
+      unless resultAnimation
+        console.warn "Animation could not be found. Using " + @settings.defaultAnimation + "."
+        resultAnimation = @settings.defaultAnimation
+
+      resultAnimation
+
+    showPageByHref = (href, options) =>
+
+      settings = $.extend {},
+        data: null
+        method: "GET"
+        animation: null
+        # callback: null
+        $referrer: null
+      , options
+
+      unless href is '#'
+        $.ajax
+          url: href
+          data: settings.data
+          type: settings.method
+          success: (data) =>
+            firstPage = insertPages(data, settings.animation)
+
+            if firstPage
+              settings.$referrer.attr "href", "#" + firstPage.attr("id") if settings.method is "GET" and @settings.cacheGetRequests is true and settings.$referrer
+              # settings.callback true  if settings.callback
+
+          error: (data) ->
+            settings.$referrer.removeClass('active') if settings.$referrer
+            # settings.callback false  if settings.callback
+
+      else settings.$referrer.removeClass('active') if settings.$referrer
+
+    start = =>
+
+      # Store some properties in a support object
+      $.support ||= {}
+
+      $.extend $.support,
+        animationEvents: (typeof window.WebKitAnimationEvent isnt "undefined")
+        touch: (typeof window.TouchEvent isnt "undefined") and (window.navigator.userAgent.indexOf("Mobile") > -1) and @settings.useFastTouch
+        transform3d: ->
+          head = $head.get(0)
+          body = document.body
+          style = document.createElement("style")
+          style.textContent = "@media (transform-3d),(-o-transform-3d),(-moz-transform-3d),(-webkit-transform-3d){#jqt-3dtest{height:3px}}"
+          div = document.createElement("div")
+          div.id = "jqt-3dtest"
+
+          # Add to the page
+          head.appendChild style
+          body.appendChild div
+
+          # Check the result
+          result = div.offsetHeight is 3
+
+          # Clean up
+          style.parentNode.removeChild style
+          div.parentNode.removeChild div
+
+          # Pass back result
+          console.warn "Support for 3d transforms: " + result + "."
+          result
+
+      console.warn "This device does not support touch interaction, or it has been deactivated by the developer. Some features might be unavailable." unless $.support.touch
+      console.warn "This device does not support 3d animation. 2d animations will be used instead."  unless $.support.transform3d
+
+      # Add extensions
+      for i in @extensions
+        fn = extensions[i]
+        $.extend @, fn(@) if $.isFunction(fn)
+
+      # Add extensions tapHandlers
+      # addTapHandler @tapHandlers[i] for i in @tapHandlers
+
+      # Add default tapHandlers
+
+      # Create an array of stuff that needs touch event handling
+      touchSelectors.push @settings.touchSelector.concat(@settings.backSelentor,@settings.submitSelector)
+      $touchSelectors = $(touchSelectors.join(', ')).css '-webkit-touch-callout', 'none'
+
+      # Make sure we have a jqt element
+      $body = $("#jqt")
+      anatomyLessons = []
+
+      unless $body.length
+        console.warn "Could not find an element with the id “jqt”, so the body id has been set to \"jqt\". If you are having any problems, wrapping your panels in a div with the id “jqt” might help."
+        $body = $(document.body).attr("id", "jqt")
+
+      anatomyLessons.push "supports3d" if $.support.transform3d
+
+      anatomyLessons.push if @settings.useTouchScroll
+        'touchscroll'
+      else
+        'autoscroll'
+
+      anatomyLessons.push @settings.fullScreenClass, @settings.statusBar if @settings.fullScreenClass and window.navigator.standalone
+
+      # Add classes to the body, delegate all our events and trigger an orientation change
+      $body.addClass(anatomyLessons.join(" "))
+        .bind("click", clickHandler)
+        .bind("orientationchange", orientationChangeHandler)
+        .bind("submit", submitHandler)
+        .bind("tap", tapHandler)
+        .bind((if $.support.touch then "touchstart" else "mousedown"), touchStartHandler)
+        .trigger "orientationchange"
+
+      $(window).bind "hashchange", hashChangeHandler
+      startHash = location.hash
+
+      # Determine what the initial view should be
+      unless $("#jqt > .current").length
+        $currentPage = $("#jqt > *:first-child").addClass("current")
+      else
+        $currentPage = $("#jqt > .current")
+
+      console.log $currentPage
+      setHash $currentPage.attr("id")
+
+      addPageToHistory $currentPage
+
+      if $(startHash).length
+        @goTo startHash
+      else
+
+
+    orientationChangeHandler = ->
+      scrollTo 0, 0
+      orientation = (if Math.abs(window.orientation) is 90 then "landscape" else "portrait")
+      $body.removeClass("portrait landscape").addClass(orientation).trigger "turn",
+        orientation: orientation
+
+    reverseAnimation = (animation) ->
+      opposites =
+        up: "down"
+        down: "up"
+        left: "right"
+        right: "left"
+        in: "out"
+        out: "in"
+
+      opposites[animation] or animation
+
+    setHash = (hash) =>
+      location.hash = "#" + hash.replace(/^#/, "") if @settings.updateHash
+
+    submitHandler = (e, callback) ->
+      $(":focus").trigger "blur"
       e.preventDefault()
-    else
-      console.warn "No need to prevent default click behavior."
-
-    # Trigger a tap event if touchstart is not on the job
-    if $.support.touch
-      console.warn "Not converting click to a tap event because touch handler is on the job."
-    else
-      console.warn "Converting click event to a tap event because touch handlers are not present or off."
-      $(e.target).trigger "tap", e
-
-  doNavigation = (fromPage, toPage, animation, goingBack) ->
+      $form = (if (typeof (e) is "string") then $(e).eq(0) else ((if e.target then $(e.target) else $(e))))
+
+      if $form.length and $form.is(@settings.formSelector) and $form.attr("action")
+        showPageByHref $form.attr("action"),
+          data: $form.serialize()
+          method: $form.attr("method") or "POST"
+          animation: getAnimation($form)
+          callback: callback
+
+        return false
+      true
+
+    submitParentForm = ($el) ->
+      $form = $el.closest("form")

-    # Private navigationEnd callback
-    navigationEndHandler = (event) ->
-      bufferTime = tapBuffer
-      if $.support.animationEvents and animation and jQTSettings.useAnimations
-        fromPage.unbind "webkitAnimationEnd", navigationEndHandler
-        fromPage.removeClass finalAnimationName + " out inmotion"
-        toPage.removeClass finalAnimationName  if finalAnimationName
-        $body.removeClass "animating animating3d"
-
-        # Position the incoming page so toolbar is at top of
-        # viewport regardless of scroll position on from page
-        if jQTSettings.trackScrollPositions is true
-          toPage.css "top", -toPage.data("lastScroll")
-
-          # Have to make sure the scroll/style resets
-          # are outside the flow of this function.
-          setTimeout ->
-            toPage.css "top", 0
-            window.scroll 0, toPage.data("lastScroll")
-            $(".scroll", toPage).each ->
-              @scrollTop = -$(this).data("lastScroll")
-          , 0
-      else
-        fromPage.removeClass finalAnimationName + " out inmotion"
-        toPage.removeClass finalAnimationName if finalAnimationName
-        bufferTime += 260
-
-      # 'in' class is intentionally delayed,
-      # as it is our ghost click hack
-      setTimeout ->
-        toPage.removeClass "in"
-        window.scroll 0, 0
-      , bufferTime
-
-      fromPage.unselect()
-
-      # Trigger custom events
-      toPage.trigger "pageAnimationEnd",
+      if $form.length
+        warn "About to submit parent form."
+        $form.trigger "submit"
+        false
+      else
+        warn "No parent form found."
+        true
+
+    tapHandler = (e) =>
+      return true if e.isDefaultPrevented()
+
+      $el = $(e.target)
+      selectors = touchSelectors.join ','
+
+      # Find the nearest tappable ancestor
+      $el = $el.closest(selectors) unless $el.is(selectors)
+
+      # Make sure we have a tappable element
+      if not $el.length or not $el.attr("href")
+        console.warn "Could not find a link related to tapped element."
+        return true
+
+      # Init some vars
+      target = $el.attr("target")
+      hash = $el.prop("hash")
+      href = $el.attr("href")
+      params =
+        e: e
+        $el: $el
+        target: target
+        hash: hash
+        href: href
+
+      # Loop thru all handlers
+      for handler in @tapHandlers
+        supported = handler.isSupported(e, params)
+        if supported
+          console.log handler
+          flag = handler.fn(e, params)
+          return flag
+
+    touchStartHandler = (e) ->
+      $el = $(e.target)
+      selectors = touchSelectors.join(", ")
+
+      # Find the nearest tappable ancestor
+      $el = $el.closest(selectors) unless $el.is(selectors)
+
+      # Make sure we have a tappable element
+      $el.addClass "active" if $el.length and $el.attr("href")
+
+      # Remove our active class if we move
+      $el.on (if $.support.touch then "touchmove" else "mousemove"), ->
+        $el.removeClass "active"
+      $el.on "touchend", ->
+        $el.unbind "touchmove mousemove"
+
+    doNavigation = (fromPage, toPage, animation, goingBack=no) =>
+
+      # Private navigationEnd callback
+      navigationEndHandler = (event) =>
+        if $.support.animationEvents and animation and @settings.useAnimations
+          fromPage.unbind "webkitAnimationEnd", navigationEndHandler
+          fromPage.removeClass finalAnimationName + " out inmotion"
+          toPage.removeClass finalAnimationName  if finalAnimationName
+          $body.removeClass "animating animating3d"
+
+          # Position the incoming page so toolbar is at top of
+          # viewport regardless of scroll position on from page
+          if @settings.trackScrollPositions is true
+            toPage.css "top", -toPage.data("lastScroll")
+
+            # Have to make sure the scroll/style resets
+            # are outside the flow of this function.
+            setTimeout ->
+              toPage.css "top", 0
+              window.scroll 0, toPage.data("lastScroll")
+              $(".scroll", toPage).each ->
+                @scrollTop = -$(this).data("lastScroll")
+            , 0
+        else
+          fromPage.removeClass finalAnimationName + " out inmotion"
+          toPage.removeClass finalAnimationName if finalAnimationName
+
+        # 'in' class is intentionally delayed,
+        # as it is our ghost click hack
+        setTimeout ->
+          toPage.removeClass "in"
+          window.scroll 0, 0
+        , tapBuffer
+
+        fromPage.removeClass 'active'
+
+        # Trigger custom events
+        toPage.trigger "pageAnimationEnd",
+          direction: "in"
+          animation: animation
+          back: goingBack
+
+        fromPage.trigger "pageAnimationEnd",
+          direction: "out"
+          animation: animation
+          back: goingBack
+
+      # Error check current page
+      unless toPage.length
+        $('.active').removeClass('active')
+        console.warn "Target element is missing."
+        return false
+      if toPage.hasClass("current")
+        $('.active').removeClass 'active'
+        console.warn "You are already on the page you are trying to navigate to."
+        return false
+
+      # Collapse the keyboard
+      $(":focus").trigger "blur"
+
+      fromPage.trigger "pageAnimationStart",
+        direction: "out"
+        back: goingBack
+
+      toPage.trigger "pageAnimationStart",
         direction: "in"
-        animation: animation
         back: goingBack

-      fromPage.trigger "pageAnimationEnd",
-        direction: "out"
-        animation: animation
-        back: goingBack
-
-    goingBack = (if goingBack then goingBack else false)
-
-    # Error check current page
-    unless toPage.length
-      $('.active').removeClass('active')
-      warn "Target element is missing."
-      return false
-    if toPage.hasClass("current")
-      $.fn.unselect()
-      warn "You are already on the page you are trying to navigate to."
-      return false
-
-    # Collapse the keyboard
-    $(":focus").trigger "blur"
-
-    fromPage.trigger "pageAnimationStart",
-      direction: "out"
-      back: goingBack
-
-    toPage.trigger "pageAnimationStart",
-      direction: "in"
-      back: goingBack
-
-    if $.support.animationEvents and animation and jQTSettings.useAnimations
-      # Fail over to 2d animation if need be
-      if not $.support.transform3d and animation.is3d
-        warn "Did not detect support for 3d animations, falling back to " + jQTSettings.defaultAnimation + "."
-        animation.name = jQTSettings.defaultAnimation
-
-      finalAnimationName = animation.name
-      is3d = (if animation.is3d then "animating3d" else "")
-
-      # Reverse animation if need be
-      finalAnimationName = finalAnimationName.replace(/left|right|up|down|in|out/, reverseAnimation)  if goingBack
-
-      warn "finalAnimationName is " + finalAnimationName + "."
-
-      # Bind internal 'cleanup' callback
-      fromPage.bind "webkitAnimationEnd", navigationEndHandler
-      $body.addClass "animating " + is3d
-      lastScroll = window.pageYOffset
-      toPage.css "top", window.pageYOffset - (toPage.data("lastScroll") or 0)  if jQTSettings.trackScrollPositions is true
-
-      # Trigger animations
-      toPage.addClass finalAnimationName + " in current"
-      fromPage.removeClass("current").addClass finalAnimationName + " out inmotion"
-
-      if jQTSettings.trackScrollPositions
-        fromPage.data "lastScroll", lastScroll
-        $(".scroll", fromPage).each ->
-          $(this).data "lastScroll", @scrollTop
-
-    else
-      toPage.addClass "current in"
-      fromPage.removeClass "current"
-      navigationEndHandler()
-    $currentPage = toPage
-    if goingBack
-      history.shift()
-    else
-      addPageToHistory $currentPage, animation
-    setHash $currentPage.attr("id")
-    true
-
-  # Fires when the history state changes
-  hashChangeHandler = (e) ->
-    if location.hash is history[0].hash
-      console.warn "We are on the right panel."
+      if $.support.animationEvents and animation and @settings.useAnimations
+        # Fail over to 2d animation if need be
+        if not $.support.transform3d and animation.is3d
+          console.warn "Did not detect support for 3d animations, falling back to " + @settings.defaultAnimation + "."
+          animation.name = @settings.defaultAnimation
+
+        finalAnimationName = animation.name
+        is3d = (if animation.is3d then "animating3d" else "")
+
+        # Reverse animation if need be
+        finalAnimationName = finalAnimationName.replace(/left|right|up|down|in|out/, reverseAnimation)  if goingBack
+
+        console.warn "finalAnimationName is " + finalAnimationName + "."
+
+        # Bind internal 'cleanup' callback
+        fromPage.bind "webkitAnimationEnd", navigationEndHandler
+        $body.addClass "animating " + is3d
+        lastScroll = window.pageYOffset
+        toPage.css "top", window.pageYOffset - (toPage.data("lastScroll") or 0)  if @settings.trackScrollPositions is true
+
+        # Trigger animations
+        toPage.addClass finalAnimationName + " in current"
+        fromPage.removeClass("current").addClass finalAnimationName + " out inmotion"
+
+        if @settings.trackScrollPositions
+          fromPage.data "lastScroll", lastScroll
+          $(".scroll", fromPage).each ->
+            $(this).data "lastScroll", @scrollTop
+
+      else
+        toPage.addClass "current in"
+        fromPage.removeClass "current"
+        navigationEndHandler()
+      $currentPage = toPage
+      if goingBack
+        customHistory.shift()
+      else
+        addPageToHistory $currentPage, animation
+      setHash $currentPage.attr("id")
       true
-    else if location.hash is ""
-      goBack()
-      true
-    else if history[1] and location.hash is history[1].hash
-      goBack()
-      true
-    else
-      # Lastly, just try going to the ID...
-      console.warn "Could not find ID in history, just forwarding to DOM element."
-      goTo $(location.hash), jQTSettings.defaultAnimation
-
-  initHairExtensions = (options) ->
-
-    # Preload images
-    for i in jQTSettings.preloadImages
-      (new Image()).src = jQTSettings.preloadImages[i]
-
-    hairExtensions = []
-
-    # Set appropriate icon
-    # (retina display available in iOS 4.2 and later.)
-    precomposed = if jQTSettings.addGlossToIcon then '' else '-precomposed'
-    hairExtensions.push """<link rel="apple-touch-icon#{precomposed}" href="#{jQTSettings.icon}">""" if jQTSettings.icon
-    hairExtensions.push """<link rel="apple-touch-icon#{precompsed}" sizes="114x114" href="#{jQTSettings.icon4}">"""  if jQTSettings.icon4
-
-    # Set startup screen
-    hairExtensions.push """<link rel="apple-touch-startup-image" href="#{jQTSettings.startupScreen}">""" if jQTSettings.startupScreen
-
-    # Set viewport
-    hairExtensions.push """<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=0">""" if jQTSettings.fixedViewport
-
-    # Set full-screen
-    if jQTSettings.fullScreen
-      hairExtensions.push """<meta name="apple-mobile-web-app-capable" content="yes" />"""
-      hairExtensions.push """<meta name="apple-mobile-web-app-status-bar-style" content="#{jQTSettings.statusBar}">""" if jQTSettings.statusBar
-
-    # Attach hair extensions
-    $head.prepend hairExtensions.join '' if hairExtensions.length
-
-  isExternalLink: ($el) ->
-    $el.attr("target") is "_blank" or $el.attr("rel") is "external" or $el.is("a[href^=\"http://maps.google.com\"], a[href^=\"mailto:\"], a[href^=\"tel:\"], a[href^=\"javascript:\"], a[href*=\"youtube.com/v\"], a[href*=\"youtube.com/watch\"]")
-
-  getAnimation = ($el) ->
-    for i in animations
-      if $el.is(animations[i].selector)
-        animation = animations[i]
-        break
-    unless animation
-      console.warn "Animation could not be found. Using " + jQTSettings.defaultAnimation + "."
-      animation = jQTSettings.defaultAnimation
-
-    animation
-
-  showPageByHref = (href, options) ->
-
-    settings = $.extend {},
-      data: null
-      method: "GET"
-      animation: null
-      # callback: null
-      $referrer: null
-    , options
-
-    unless href is '#'
-      $.ajax
-        url: href
-        data: settings.data
-        type: settings.method
-        success: (data) ->
-          firstPage = insertPages(data, settings.animation)
-
-          if firstPage
-            settings.$referrer.attr "href", "#" + firstPage.attr("id") if settings.method is "GET" and jQTSettings.cacheGetRequests is true and settings.$referrer
-            # settings.callback true  if settings.callback
-
-        error: (data) ->
-          settings.$referrer.removeClass('active') if settings.$referrer
-          # settings.callback false  if settings.callback
-
-    else settings.$referrer.removeClass('active') if settings.$referrer
-
-  start = ->
-
-    # Store some properties in a support object
-    $.support ||= {}
-
-    $.extend $.support,
-      animationEvents: (typeof window.WebKitAnimationEvent isnt "undefined")
-      touch: (typeof window.TouchEvent isnt "undefined") and (window.navigator.userAgent.indexOf("Mobile") > -1) and jQTSettings.useFastTouch
-      transform3d: ->
-        head = $head.get(0)
-        body = document.body
-        style = document.createElement("style")
-        style.textContent = "@media (transform-3d),(-o-transform-3d),(-moz-transform-3d),(-webkit-transform-3d){#jqt-3dtest{height:3px}}"
-        div = document.createElement("div")
-        div.id = "jqt-3dtest"
-
-        # Add to the page
-        head.appendChild style
-        body.appendChild div
-
-        # Check the result
-        result = div.offsetHeight is 3
-
-        # Clean up
-        style.parentNode.removeChild style
-        div.parentNode.removeChild div
-
-        # Pass back result
-        console.warn "Support for 3d transforms: " + result + "."
-        result
-
-    console.warn "This device does not support touch interaction, or it has been deactivated by the developer. Some features might be unavailable." unless $.support.touch
-    console.warn "This device does not support 3d animation. 2d animations will be used instead."  unless $.support.transform3d
-
-    # Add extensions
-    for i in extensions
-      fn = extensions[i]
-      $.extend publicObj, fn(publicObj) if $.isFunction(fn)
-
-    # Add extensions tapHandlers
-    addTapHandler extTapHandlers[i] for i in extTapHandlers
-
-    # Add default tapHandlers
-    addDefaultTapHandlers()
-
-    # Add animations
-    for i in defaults.animations
-      animation = defaults.animations[i]
-      animation.selector = jQTSettings[animation.name + "Selector"]  if jQTSettings[animation.name + "Selector"] isnt `undefined`
-      addAnimation animation
-
-    # Create an array of stuff that needs touch event handling
-    touchSelectors.push jQTSettings.touchSelector.concat(jQTSettings.backSelentor,jQTSettings.submitSelector)
-    $touchSelectors = $(touchSelectors.join(', ')).css '-webkit-touch-callout', 'none'
-
-    # Make sure we have a jqt element
-    $body = $("#jqt")
-    anatomyLessons = []
-
-    unless $body.length
-      console.warn "Could not find an element with the id “jqt”, so the body id has been set to \"jqt\". If you are having any problems, wrapping your panels in a div with the id “jqt” might help."
-      $body = $(document.body).attr("id", "jqt")
-
-    anatomyLessons.push "supports3d" if $.support.transform3d
-
-    anatomyLessons.push if jQTSettings.useTouchScroll
-      'touchscroll'
-    else
-      'autoscroll'
-
-    anatomyLessons.push jQTSettings.fullScreenClass, jQTSettings.statusBar if jQTSettings.fullScreenClass and window.navigator.standalone
-
-    # Add classes to the body, delegate all our events and trigger an orientation change
-    $body.addClass(anatomyLessons.join(" "))
-      .bind("click", clickHandler)
-      .bind("orientationchange", orientationChangeHandler)
-      .bind("submit", submitHandler)
-      .bind("tap", tapHandler)
-      .bind((if $.support.touch then "touchstart" else "mousedown"), touchStartHandler)
-      .trigger "orientationchange"
-
-    $(window).bind "hashchange", hashChangeHandler
-    startHash = location.hash
-
-    # Determine what the initial view should be
-    if $("#jqt > .current").length
-      $currentPage = $("#jqt > *:first-child").addClass("current")
-    else
-      $currentPage = $("#jqt > .current")
-
-    setHash $currentPage.attr("id")
-    addPageToHistory $currentPage
-    goTo startHash  if $(startHash).length
-
-  orientationChangeHandler = ->
-    scrollTo 0, 0
-    orientation = (if Math.abs(window.orientation) is 90 then "landscape" else "portrait")
-    $body.removeClass("portrait landscape").addClass(orientation).trigger "turn",
-      orientation: orientation
-
-  reverseAnimation = (animation) ->
-    opposites =
-      up: "down"
-      down: "up"
-      left: "right"
-      right: "left"
-      in: "out"
-      out: "in"
-
-    opposites[animation] or animation
-
-  setHash = (hash) ->
-    location.hash = "#" + hash.replace(/^#/, "") if jQTSettings.updateHash
-
-  submitHandler = (e, callback) ->
-    $(":focus").trigger "blur"
-    e.preventDefault()
-    $form = (if (typeof (e) is "string") then $(e).eq(0) else ((if e.target then $(e.target) else $(e))))
-
-    if $form.length and $form.is(jQTSettings.formSelector) and $form.attr("action")
-      showPageByHref $form.attr("action"),
-        data: $form.serialize()
-        method: $form.attr("method") or "POST"
-        animation: getAnimation($form)
-        callback: callback
-
-      return false
-    true
-
-  submitParentForm = ($el) ->
-    $form = $el.closest("form")
-
-    if $form.length
-      warn "About to submit parent form."
-      $form.trigger "submit"
-      false
-    else
-      warn "No parent form found."
-      true
-
-  tapHandler = (e) ->
-    return true if e.isDefaultPrevented()
-
-    # Find the nearest tappable ancestor
-    $el = $el.closest(selectors) unless $el.is(selectors)
-
-    # Make sure we have a tappable element
-    if not $el.length or not $el.attr("href")
-      console.warn "Could not find a link related to tapped element."
-      return true
-
-    # Init some vars
-    target = $el.attr("target")
-    hash = $el.prop("hash")
-    href = $el.attr("href")
-    params =
-      e: e
-      $el: $el
-      target: target
-      hash: hash
-      href: href
-      jQTSettings: jQTSettings
-
-
-    # Loop thru all handlers
-    for i in tapHandlers
-      handler = tapHandlers[i]
-      supported = handler.isSupported(e, params)
-      if supported
-        flag = handler.fn(e, params)
-        return flag
-
-  touchStartHandler = (e) ->
-    $el = $(e.target)
-    selectors = touchSelectors.join(", ")
-
-    # Find the nearest tappable ancestor
-    $el = $el.closest(selectors) unless $el.is(selectors)
-
-    # Make sure we have a tappable element
-    $el.addClass "active" if $el.length and $el.attr("href")
-
-    # Remove our active class if we move
-    $el.on (if $.support.touch then "touchmove" else "mousemove"), ->
-      $el.removeClass "active"
-    $el.on "touchend", ->
-      $el.unbind "touchmove mousemove"
-
-  # Initialize the class
-  jQTSettings = $.extend({}, defaults, options)
-  initHairExtensions options
-  initFXExtensions()
-  jQTSettings.starter start+    # FINAL PART OF CONSTRUCTOR
+
+    # Apply the options against our defaults
+    @settings = $.extend({}, @defaults, options)
+
+    # Add meta tags and body classes
+    initHairExtensions()
+
+    @settings.starter start


:lipstick:

:lipstick:

--- src/project.coffee
+++ src/project.coffee
@@ -74,7 +74,6 @@
         throw error unless error.code is 'EISDIR'
     params

-
   ###
   Section: Event Subscription
   ###


Forgot to remove

Forgot to remove

--- resources/assets/coffee/main.coffee
+++ resources/assets/coffee/main.coffee
@@ -50,7 +50,6 @@
 @osuAudio ?= new OsuAudio
 @osuLayzr ?= new OsuLayzr
 @parentFocus ?= new ParentFocus
-@password ?= new Password
 @postPreview ?= new PostPreview
 @reactTurbolinks ||= new ReactTurbolinks
 @replyPreview ?= new ReplyPreview
@@ -62,7 +61,6 @@
 @timeago ?= new Timeago
 @tooltipDefault ?= new TooltipDefault
 @twitchPlayer ?= new TwitchPlayer
-@userEmail ?= new UserEmail
 @wiki ?= new Wiki

 @formConfirmation ?= new FormConfirmation(@formError)


test/param.coffee @Radagaisus #43

test/param.coffee @Radagaisus #43

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

--- tests/param.coffee
+++ tests/param.coffee
@@ -0,0 +1,52 @@
+zappa = require '../src/zappa'
+port = 15300
+
+@tests =
+  # Test for express app.param
+  param: (t) ->
+    t.expect 1,2,3,4,5,6
+    t.wait 4000
+    zapp = zappa port++, ->
+
+      @param 'user_id': ->
+        if not @param.match /^[0-9A-F]{24,24}$/i
+          @next "Invalid user_id"
+        else @next()
+
+      @param
+        'user': ->
+          if not @param.match /^[0-9A-F]{24,24}$/i
+            @next "Invalid user"
+          else @next()
+        'camper': ->
+          if not @param.match /^[0-9A-F]{24,24}$/i
+            @next "Invalid camper"
+          else @next()
+
+      @get '/:user_id', ->
+        @send @params.user_id
+
+      @get '/camper/:camper', ->
+        @send @params.camper
+
+      @get '/user/:user', ->
+        @send @params.user
+
+    c = t.client(zapp.server)
+    c.get '/no-such-user-id', (err, res) ->
+      t.equal 1, res.body, 'Invalid user_id'
+
+    c.get '/123456789012345678901234', (err, res) ->
+      t.equal 2, res.body, '123456789012345678901234'
+
+    c.get '/camper/no-such-user-id', (err, res) ->
+      t.equal 3, res.body, 'Invalid camper'
+
+    c.get '/camper/123456789012345678901234', (err, res) ->
+      t.equal 4, res.body, '123456789012345678901234'
+
+    c.get '/user/no-such-user-id', (err, res) ->
+      t.equal 5, res.body, 'Invalid user'
+
+    c.get '/user/123456789012345678901234', (err, res) ->
+      t.equal 6, res.body, '123456789012345678901234'


Attempt to fix indentation

Attempt to fix indentation

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/Arm1stice/prosu.git/'


wip: add tupelo-main.coffee

wip: add tupelo-main.coffee

error: pathspec 'www/tupelo-main.coffee' did not match any file(s) known to git

--- www/tupelo-main.coffee
+++ www/tupelo-main.coffee
@@ -0,0 +1,545 @@
+### tupelo-main.coffee
+# vim: sts=2 sw=2 et:
+###
+
+$ = jQuery
+
+$(document).ready ->
+  # status object
+  tupelo =
+    game_state: {}
+    events: []
+
+  states =
+    initial:
+      show: [ "#register_form" ]
+      hide: [ "#quit_form", "#games", "#players", "#my_game", "#game" ]
+      change: ->
+        reg = $("#register_name")
+        reg.addClass "initial"
+        reg.val "Your name"
+        return
+
+    registered:
+      show: [ "#quit_form", "#games", "#players", "#game_create_form" ]
+      hide: [ "#register_form", "#my_game", "#game" ]
+      change: ->
+        tupelo.list_timer = setInterval(updateLists, 5000)  if tupelo.list_timer is `undefined`
+
+    gameCreated:
+      show: [ "#my_game" ]
+      hide: [ "#game_create_form" ]
+
+    inGame:
+      show: [ "#game" ]
+      hide: [ "#games", "#players", "#my_game" ]
+
+  setState = (state, effectDuration) ->
+    st = states[state]
+    $(elem).hide effectDuration for elem in st.hide
+    $(elem).show effectDuration for elem in st.show
+    st.change() if st.change?
+
+  escapeHtml = (str) ->
+    str.replace("<", "<").replace ">", ">"
+
+  dbg = ->
+    $("#debug").html JSON.stringify(tupelo) if T.debug
+
+  ajaxErr = (xhr, astatus, error) ->
+    handled = false
+    # 403 is for game and rule errors
+    if xhr.status is 403
+      # parse from json
+      jsonErr = eval "(" + xhr.responseText + ")"
+      if jsonErr.message?
+        window.alert jsonErr.message
+        handled = true
+    else
+      T.log "status: " + astatus
+      T.log "error: " + error
+    handled
+
+  hello = ->
+    $.ajax
+      url: "/ajax/hello"
+      success: (result) ->
+        T.log "Server version: " + result.version
+        # are we already logged in?
+        if result.player?
+          tupelo.player = new T.Player result.player.player_name
+          registerOk result.player
+          # are we in a game?
+          gameCreateOk result.player.game_id if result.player.game_id?
+
+      error: ajaxErr
+
+  updateGameLinks = (disabledIds) ->
+    gameJoinClicked = (event) ->
+      # "game_join_ID"
+      game_id = @id[10..]
+      T.log "joining game " + game_id
+      $.ajax
+        url: "/ajax/game/enter"
+        data:
+          akey: tupelo.player.akey
+          game_id: game_id
+
+        success: gameCreateOk
+        error: ajaxErr
+
+    if tupelo.game_id?
+      $("button.game_join").attr "disabled", true
+      $("tr#game_id_" + tupelo.game_id).addClass "highlight"
+    else
+      $("button#" + id).attr "disabled", true for id in disabledIds
+      $("button.game_join").click gameJoinClicked
+
+  listGamesOk = (result) ->
+    #T.log "listGamesOk"
+    #T.log result
+    html = ""
+    disabledIds = []
+    for own game_id of result
+      html += "
"
+      #players = []
+      #for res in result[game_id]
+      #  plr = new T.Player().fromObj res
+      #  players.push plr.player_name
+      players = (new T.Player().fromObj(res).player_name for res in result[game_id])
+
+      html += "" + escapeHtml(players.join(", ")) + "	"
+      html += "<button class=\"game_join btn\" id=\"game_join_" + game_id + "\">join</button>	"
+      html += "
"
+      disabledIds.push "game_join_" + game_id if players.length is 4
+
+    $("#game_list table tbody").html html
+    updateGameLinks disabledIds
+    dbg()
+
+  listPlayersOk = (result) ->
+    T.log result
+    html = ""
+    for own player of result
+      plr = new T.Player().fromObj(result[player])
+      if plr.id isnt tupelo.player.id
+        cls = if plr.game_id? then "class=\"in_game\" " else ""
+        html += "
"
+        html += "" + escapeHtml(plr.player_name) + "
"
+
+    $("#player_list table tbody").html html
+    dbg()
+
+  registerOk = (result) ->
+    $("#name").val ""
+    tupelo.player.id = result.id
+    tupelo.player.akey = result.akey
+    $.cookie "akey", result.akey
+    T.log tupelo
+    dbg()
+    # clear game list if there was one previously
+    $("#game_list table tbody").html ""
+    $(".my_name").html escapeHtml(tupelo.player.player_name)
+    updateLists()
+    setState "registered", "fast"
+    T.log "timer created"
+
+  leftGame = ->
+    if tupelo.event_fetch_timer?
+      tupelo.event_fetch_timer.disable()
+      tupelo.event_fetch_timer = null
+
+    if tupelo.event_timer?
+      clearTimeout tupelo.event_timer
+      tupelo.event_timer = null
+
+    tupelo.game_id = null
+    tupelo.game_state = {}
+    tupelo.hand = null
+    tupelo.my_turn = null
+    return
+
+  leaveOk = (result) ->
+    leftGame()
+    dbg()
+    updateLists()
+    setState "registered", "fast"
+
+  quitOk = (result) ->
+    leftGame()
+    if tupelo.list_timer?
+      clearInterval tupelo.list_timer
+      tupelo.list_timer = null
+
+    $.cookie "akey", null
+    tupelo.player = null
+    T.log tupelo
+    dbg()
+    setState "initial", "fast"
+
+  gameCreateOk = (result) ->
+    tupelo.game_id = result
+    T.log tupelo
+    dbg()
+    $("p#joined_game").html "joined game " + tupelo.game_id
+    setState "gameCreated", "fast"
+    tupelo.event_fetch_timer = new T.Timer("/ajax/get_events", 2000, eventsOk,
+      data:
+        akey: tupelo.player.akey
+      )
+    updateLists()
+
+  cardPlayed = (event) ->
+    T.log "cardPlayed"
+    #T.log event
+    table = $("#player_" + event.player.id + " .table")
+    if table.length is 0
+      T.log "player not found!"
+      return true
+
+    #table.hide()
+    card = new T.Card(event.card.suit, event.card.value)
+    table.html "" + card.toShortHtml() + ""
+    table.children().show 500
+    setTimeout processEvent, 500
+    false
+
+  messageReceived = (event) ->
+    T.log "messageReceived"
+    eventLog = $("#event_log")
+    eventLog.append escapeHtml(event.sender) + ": " + escapeHtml(event.message) + "\n"
+    eventLog.scrollTop eventLog[0].scrollHeight - eventLog.height()
+    true
+
+  # clear the table and resume event processing
+  # called either from timeout or click event
+  clearTable = ->
+    clearTimeout tupelo.clear_timeout
+    T.log "clearing table"
+    $("#game_area .table").html ""
+    $("#game_area table tbody").unbind "click"
+    # re-enable event processing
+    tupelo.event_timer = setTimeout(processEvent, 0)
+    #T.log("trickPlayed: set event_timer to " + tupelo.event_timer)
+
+  trickPlayed = (event) ->
+    T.log "trickPlayed"
+    # TODO: highlight the highest card
+    # allow the user to clear the table and proceed by clicking the table
+    $("#game_area table tbody").click clearTable
+    # setting timeout to show the played trick for a longer time
+    tupelo.clear_timeout = setTimeout(clearTable, 5000)
+    false # this event is not handled yet
+
+  getTeamPlayers = (team) ->
+    # TODO: should we store these in JS instead of the DOM?
+    [ $("#table_" + team + " .player_name").html(), $("#table_" + (team + 2) + " .player_name").html() ]
+
+  updateGameState = (state) ->
+    statusStr = ""
+    for own key of state
+      tupelo.game_state[key] = state[key]
+
+    # show game status (voting, nolo, rami)
+    if state.state is T.VOTING
+      statusStr = "VOTING"
+    else if state.state is T.ONGOING
+      if state.mode is T.NOLO
+        statusStr = "NOLO"
+      else statusStr = "RAMI" if state.mode is T.RAMI
+
+    statusStr = "" + statusStr + ""
+    statusStr += "tricks: " + state.tricks[0] + " - " + state.tricks[1] + ""
+
+    if state.score?
+      if state.score[0] > 0
+        statusStr += "score: " + escapeHtml(getTeamPlayers(0).join(" & ")) + ": " + state.score[0] + ""
+      else if state.score[1] > 0
+        statusStr += "score: " + escapeHtml(getTeamPlayers(1).join(" & ")) + ": " + state.score[1] + ""
+      else
+        statusStr += "score: 0"
+
+    $("#game_status").html statusStr
+    # highlight the player in turn
+    if state.turn_id?
+      $(".player_data .player_name").removeClass "highlight_player"
+      $("#player_" + state.turn_id + " .player_name").addClass "highlight_player"
+
+    dbg()
+
+  cardClicked = (event) ->
+    # id is "card_X"
+    cardId = $(this).find(".card").attr("id").slice(5)
+    card = tupelo.hand[cardId]
+    T.log card
+    if card?
+      $.ajax
+        url: "/ajax/game/play_card"
+        success: (result) ->
+          tupelo.my_turn = false
+          $("#hand .card").removeClass "card_selectable"
+          # TODO: after playing a hand, this shows the next hand too quickly
+          getGameState()
+
+        error: (xhr, astatus, error) ->
+          window.alert xhr.status + " " + error if ajaxErr(xhr, astatus, error) isnt true
+
+        data:
+          akey: tupelo.player.akey
+          game_id: tupelo.game_id
+          card: JSON.stringify(card)
+
+    event.preventDefault()
+
+  updateHand = (newHand) ->
+    html = ""
+    hand = []
+    for item in newHand
+      card = new T.Card(item.suit, item.value)
+      hand.push card
+      html += "" if tupelo.my_turn
+      html += "" + card.toShortHtml() + ""
+      html += "" if tupelo.my_turn
+
+    tupelo.hand = hand
+    $("#hand").html html
+    if tupelo.my_turn
+      $("#hand .card").addClass "card_selectable"
+      $("#hand a").click cardClicked
+
+  getGameState = ->
+    $.ajax
+      url: "/ajax/game/get_state"
+      success: (result) ->
+        T.log result
+        updateGameState result.game_state if result.game_state?
+        updateHand result.hand if result.hand?
+
+      error: ajaxErr
+      data:
+        akey: tupelo.player.akey
+        game_id: tupelo.game_id
+
+  turnEvent = (event) ->
+    T.log "turnEvent"
+    tupelo.my_turn = true # enables click callbacks for cards in hand
+    getGameState()
+    true
+
+  stateChanged = (event) ->
+    T.log "stateChanged"
+    if event.game_state.state is T.VOTING # game started!
+      startOk()
+    else if event.game_state.state is T.ONGOING # VOTING -> ONGOING
+      # allow the user to clear the table and proceed by clicking the table
+      $("#game_area table tbody").click clearTable
+      # setting timeout to show the voted cards for a longer time
+      tupelo.clear_timeout = setTimeout(clearTable, 5000)
+      return false
+    true
+
+  processEvent = ->
+    handled = true
+    if tupelo.events.length is 0
+      T.log "no events to process"
+      tupelo.event_timer = null
+      return
+
+    event = tupelo.events.shift()
+    updateGameState event.game_state if event.game_state?
+    switch event.type
+      when 1
+        handled = cardPlayed(event)
+      when 2
+        handled = messageReceived(event)
+      when 3
+        handled = trickPlayed(event)
+      when 4
+        handled = turnEvent(event)
+      when 5
+        handled = stateChanged(event)
+      else
+        T.log "unknown event " + event.type
+
+    tupelo.event_timer = setTimeout(processEvent, 0) if handled is true
+
+  eventsOk = (result) ->
+    ###
+    if (result.length > 0)
+      eventLog = $("#event_log")
+      eventLog.append(JSON.stringify(result) + "\n")
+      eventLog.scrollTop(eventLog[0].scrollHeight - eventLog.height())
+    ###
+    # push events to queue
+    tupelo.events.push event for event in result
+
+    tupelo.event_timer = setTimeout(processEvent, 0) if tupelo.events.length > 0 and tupelo.event_timer is `undefined`
+    dbg()
+
+  gameInfoOk = (result) ->
+    T.log "gameInfoOk"
+    T.log result
+    myIndex = 0
+    # find my index
+    for pl, i in result
+      if pl.id is tupelo.player.id
+        myIndex = i
+        break
+
+    for pl, i in result
+      # place where the player goes when /me is always at the bottom
+      index = (4 + i - myIndex) % 4
+      # set player id and name
+      $("#table_" + index + " .player_data").attr "id", "player_" + pl.id
+      $("#player_" + pl.id + " .player_name").html escapeHtml(pl.player_name)
+
+    return
+
+  startOk = (result) ->
+    if tupelo.list_timer?
+      clearInterval tupelo.list_timer
+      tupelo.list_timer = null
+
+    $("#event_log").html ""
+    setState "inGame"
+    $.ajax
+      url: "/ajax/game/get_info"
+      success: gameInfoOk
+      error: ajaxErr
+      data:
+        game_id: tupelo.game_id
+
+    getGameState()
+    dbg()
+
+  updateLists = ->
+    $.ajax
+      url: "/ajax/game/list"
+      success: listGamesOk
+      error: ajaxErr
+
+    $.ajax
+      url: "/ajax/player/list"
+      success: listPlayersOk
+      error: ajaxErr
+      data:
+        akey: tupelo.player.akey
+
+$("#echo_ajax").click ->
+  text = $("#echo").val()
+  $.ajax
+    url: "/ajax/echo"
+    data:
+      test: text
+
+    success: (result) ->
+      $("#echo_result").html escapeHtml(result)
+      $("#echo").val ""
+
+    error: ajaxErr
+
+  $("#register_btn").click ->
+    input = $("#register_name")
+    name = input.val()
+    if not name or input.hasClass("initial")
+      alert "Please enter your name first"
+      return
+    tupelo.player = new T.Player(name)
+    T.log tupelo
+    $.ajax
+      url: "/ajax/player/register"
+      data:
+        player: JSON.stringify(tupelo.player)
+
+      success: registerOk
+      error: ajaxErr
+
+  # sign in by pressing enter
+  $("#register_name").keyup (event) ->
+    $("#register_btn").click() if (event.keyCode or event.which) is 13
+
+  $("#register_name").focus (event) ->
+    if $(this).hasClass("initial")
+      $(this).val ""
+      $(this).removeClass "initial"
+
+  $("#quit_btn").click ->
+    $.ajax
+      url: "/ajax/player/quit"
+      data:
+        akey: tupelo.player.akey
+
+      success: quitOk
+      error: (xhr, astatus, error) ->
+        quitOk()  if ajaxErr(xhr, astatus, error) is true
+
+  $(".game_leave_btn").click ->
+    # TODO: should we cancel timers already here?
+    $.ajax
+      url: "/ajax/game/leave"
+      data:
+        akey: tupelo.player.akey
+        game_id: tupelo.game_id
+
+      success: leaveOk
+      error: (xhr, astatus, error) ->
+        leaveOk()  if ajaxErr(xhr, astatus, error) is true
+
+  $("#game_create_btn").click ->
+    # TODO: should we cancel timers already here?
+    $.ajax
+      url: "/ajax/game/create"
+      data:
+        akey: tupelo.player.akey
+
+      success: gameCreateOk
+      error: ajaxErr
+
+  $("#game_start").click ->
+    $.ajax
+      url: "/ajax/game/start"
+      data:
+        akey: tupelo.player.akey
+        game_id: tupelo.game_id
+
+      success: startOk
+      error: ajaxErr
+
+  $("#game_start_with_bots").click ->
+    $.ajax
+      url: "/ajax/game/start_with_bots"
+      data:
+        akey: tupelo.player.akey
+        game_id: tupelo.game_id
+
+      success: startOk
+      error: ajaxErr
+
+  if T.debug is true
+    $("#debug").click ->
+      $(this).toggle()
+  else
+    $("#debug").hide()
+
+  # leave the game when the user leaves the page
+  $(window).unload ->
+    if tupelo.game_id?
+      $.ajax
+        url: "/ajax/game/leave"
+        async: false
+        data:
+          akey: tupelo.player.akey
+          game_id: tupelo.game_id
+
+  # show a confirmation if the browser supports it
+  window.onbeforeunload = (e) ->
+    if tupelo.game_id is `undefined`
+      return `undefined` # no dialog
+    e = e or window.event
+    msg = "By leaving the page you will also leave the game."
+    e.returnValue = msg if e
+    msg
+
+  hello()
+  setState "initial"
+


Add onload parsing and navigation handling.

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/JoshuaToenyes/stout-client.git/'


Create render.coffee

Create render.coffee

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

--- render.coffee
+++ render.coffee
@@ -0,0 +1,209 @@
+# Released under MIT license
+# Copyright (c) 2013 Dino Reic (@dux)
+# https://github.com/dux/render
+
+# $(node).render(opts, data)
+# Render.to(node, opts, data)
+#  node: '#node' || [DOM object] || [jQuery object]
+#  opts:
+#    template
+#      #data           : takes .html()
+#      /ajax.html      : $.get and render
+#      f(data) {}      : $.get and render
+#
blah

+#    script: [scripts to preload]
+#    bind:
+#      el_on_state_change:(node, value) ->
+#         function data
+#    data:
+#
+#  data: { el1:1, el2:2, ... }
+#
+
+# useful jQuery plugins
+$.extend
+  cachedScript: (url, options) ->
+    if typeof(options) == "function"
+      options =
+        success:options
+
+    options = $.extend options or {},
+      crossDomain: true
+      dataType: "script"
+      cache: true
+      url: url
+
+    $.ajax options
+
+$.fn.extend
+  exportByAttr: (name, func) ->
+    this.find("*[#{name}]").each ->
+      el = $ this
+      val = el.attr(name)
+      func(el, val)
+      el
+
+  serializeHash: ->
+    stringKey = (key, value) ->
+      beginBracket = key.lastIndexOf("[")
+      if beginBracket is -1
+        hash = {}
+        hash[key] = value
+        return hash
+      newKey = key.substr(0, beginBracket)
+      newValue = {}
+      newValue[key.substring(beginBracket + 1, key.length - 1)] = value
+      stringKey newKey, newValue
+    hash = {}
+    els = $(this).find(":input").get()
+    $.each els, ->
+      if @name and not @disabled and (@checked or /select|textarea/i.test(@nodeName) or /hidden|text|search|tel|url|email|password|datetime|date|month|week|time|datetime-local|number|range|color/i.test(@type))
+        val = $(this).val()
+        $.extend true, hash, stringKey(@name, val)
+
+    hash
+
+@Render =
+  is_link: (data) ->
+    return true if /^http/.test(data)
+    return true if /^\//.test(data)
+    false
+
+  scope: (node) ->
+    $(node).closest('.render-box').data('scope')
+
+  to: (node, opts, data) ->
+    opts.body     = $ node
+    opts.data     = data if data
+    opts.data     ||= {}
+    opts.node     ||= {}
+    opts.bind     ||= {}
+    opts.script   ||= []
+    opts.once     ||= -> true
+    opts.onchange ||= -> true
+    template = opts.template
+
+    # script: '...' to script: ['...']
+    if opts.script && typeof opts.script == 'string'
+      opts.script= [opts.script]
+
+    # load all scripts and then execute widget
+    while script = opts.script.shift()
+      $.cachedScript script, =>
+        @to(opts.body, opts)
+      return
+
+    # execute template if it is function, pass data
+    if typeof(template) == 'function'
+      template = template(opts.data)
+
+    # if template is object, extract data
+    if typeof(template) == 'object' || /^#\w/.test(template)
+      template = $(template).html()
+
+    # if it is a link, ajax get
+    else if @is_link(template)
+      $.get template, (ret) =>
+        opts.template = ret
+        @to(opts.body, opts)
+      return
+
+    # if everything else fails, extract template from target and show it
+    unless template
+      template = opts.template = node.html()
+      node.show()
+
+    template = template.replace(/\$scope\./g,'Render.scope(this).')
+
+    opts.set = (key, val) ->
+      opts.data[key] = val
+      node = @node[key]
+      if typeof node == 'object'
+        if ['TEXTAREA', 'INPUT', 'SELECT'].indexOf(node[0].nodeName) > -1
+          node.val val
+        else
+          node.html val
+
+      opts.onchange()
+      Render.onchange_default(opts)
+
+    @render opts
+
+
+  render: (opts)->
+
+    opts.body.html "
#{opts.template}
"
+    opts.body.find('div').first().data('scope', opts)
+
+    opts.body.exportByAttr 'bind', (node, bind_var) =>
+      node_name = node[0].nodeName.toLowerCase()
+      keys = bind_var.split('-', 2)
+      bind_var = keys.shift()
+
+      if ['data', 'template', 'bind', 'once', 'onchange','node', 'script'].indexOf(bind_var) > -1
+        alert "Render error: [#{bind_var}] for bind= is reserved word"
+        return
+
+      opts.node[bind_var] = node
+
+      if node_name == 'button'
+        node.on 'click', =>
+          opts.set bind_var, keys[0]
+          opts.bind[bind_var].apply(opts, [node, keys[0]]) if opts.bind[bind_var]
+      else if node_name == 'input' || node_name == 'textarea'
+        if node[0].type == 'checkbox'
+          node.after("<input style='display:none;' name='#{bind_var}' />")
+          node.on 'click', ->
+            opts.set bind_var, node[0].checked
+            node.next().val if node[0].checked then 1 else 0
+        else
+          opts.data[bind_var] = node.val()
+          node.on 'keyup', ->
+            opts.set bind_var, node.val()
+          if opts.bind[bind_var]
+            node.on 'focus', ->
+            opts.bind[bind_var](node, keys[0]) if opts.bind[bind_var]
+      else if node_name == 'form'
+        node.on 'submit', =>
+          opts.bind[bind_var].apply(opts, [node, node.serializeHash()]) if opts.bind[bind_var]
+          @onchange_default(opts)
+          false
+
+    opts.once()
+    opts.onchange()
+    @onchange_default(opts)
+
+
+  onchange_default: (opts) ->
+    for key,node of opts.node
+      if ['TEXTAREA', 'INPUT', 'SELECT'].indexOf(node[0].nodeName) > -1
+        node.val opts.data[key]
+      else
+        node.html opts.data[key]
+
+    opts.body.exportByAttr 'if', (node, bind_var) =>
+      inverse = false
+      bind_var = bind_var.replace(/\s+/g,'').split('!')
+      if bind_var[1]
+        inverse = true
+        bind_var = bind_var[1]
+
+      bind_val = opts.data[bind_var]
+      (=>
+        return node.show() if typeof bind_val == 'object' && bind_val.length > 0
+        return node.show() if bind_val
+        node.hide()
+      )()
+      display_now = node.css('display')
+      node.css(display:if display_now == 'block' then 'none' else 'block') if inverse
+
+
+  refresh: (node) ->
+    root = $(node).closest('.render-box')
+    opts = root.data('scope')
+    root.parent().render(opts)
+
+$.fn.render = (opts, data) ->
+  Render.to(this, opts, data)
+  this
+


forgot this

forgot this

error: pathspec 'src/lib/crypt.iced' did not match any file(s) known to git

--- src/lib/crypt.iced
+++ src/lib/crypt.iced
@@ -0,0 +1,118 @@
+
+purepack = require 'purepack'
+
+##=======================================================================
+
+# Fulfill the CryptoJS template
+Binary =
+  stringify : (wa) ->
+    [v,n] = [wa.words, wa.sigBytes]
+    e = String.fromCharCode
+    (e((v[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff) for i in [0...n]).join ''
+
+# Fulfill the CryptoJS template -- we're abusing the 'stringify' interface,
+# but it's OK for now...
+exports.Ui8a = Ui8a =
+
+  stringify : (wa) ->
+    [v,n] = [wa.words, wa.sigBytes]
+    out = new Uint8Array n
+    (out[i] = ((v[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff) for i in [0...n])
+    return out
+
+  to_word_array : (uia) ->
+    n = uia.length
+    nw = (n >>> 2)  + (if (n & 0x3) then 1 else 0)
+    out = new Int32Array nw
+    out[i] = 0 for i in [0...nw]
+    for b, i in uia
+      out[i >>> 2] |= (b << (24 - (i % 4)*8))
+    out
+
+##=======================================================================
+
+exports.Cryptor = class Cryptor
+
+  ##-----------------------------------------
+
+  constructor : (@_aes_key, @_mac_key) ->
+    @_errors = []
+    @_mac_errors =  0
+    @_decode_errors = 0
+    @_aes_errors = 0
+    @_successes = 0
+
+  ##-----------------------------------------
+
+  finish : () ->
+    out = null
+    if @_errors.length?
+      out = @_errors
+      @_errors = []
+    out
+
+  ##-----------------------------------------
+
+  hit_error : (error, value, type) ->
+    @_errors.push { error, value, type }
+
+  ##-----------------------------------------
+
+  verify_mac : (obj, received) ->
+    packed = purepack.pack obj, ENCODING
+    macer = C.algo.HMAC.create C.algo.SHA256, @_mac_key
+    computed = macer.update(packed).finalize().toString Binary
+    return (computed is received)
+
+  ##-----------------------------------------
+
+  # how to encrypt:
+  #   pick random iv with prng...
+  #   C.AES.encrypt msg, key, { iv }
+  #   i think msg and key should both be WordArrays...
+  decrypt_aes : (iv, ctxt) ->
+    # Create a cryptoJS object
+    #  - iv is an array of signed int32s, that we get from msgpack.
+    #    It should be 4 words long, as per AES-256-CBC mode
+    #  - ciphertext is an array of 32-bit words
+    iv = C.lib.WordArray.create iv
+    ctxt = C.lib.WordArray.create ctxt
+    cfg = { iv }
+    cp = C.lib.CipherParams.create { ciphertext : ctxt }
+    C.AES.decrypt cp, @_aes_key, cfg
+
+  ##-----------------------------------------
+
+  decrypt : (v, name) ->
+    ret = null
+    try
+      unpacked = purepack.unpack v, ENCODING
+    catch e
+      err = e
+    if err?
+      @hit_error err, v, name
+      @_decode_errors++
+    else if not (Array.isArray unpacked)
+      @hit_error "needed an array", unpacked.toString(), name
+      @_decode_errors++
+    else if unpacked[0] isnt 1
+      @hit_error "only can decode version 1", unpacked[0], name
+      @_decode_errors++
+    else if unpacked.length isnt 4
+      @hit_error "needed 4 fields in array", unpacked.length, name
+      @_decode_errors++
+    else if not @verify_mac unpacked[2..3], unpacked[1]
+      @hit_error "MAC mismatch", unpacked.toString(), name
+      @_mac_errors++
+    else if not (pt = @decrypt_aes unpacked[2], unpacked[3], name)?
+      @hit_error "Decrypt failure", unpacked.toString(), name
+      @_aes_errors++
+    else if not (ui8a = pt.stringify Ui8a)? or
+            not ([err, unpacked] = purepack.unpack ui8a, 'ui8a')? or err?
+      @hit_error "Failed to decode plaintext", pt, name
+      @_decode_errors++
+    else
+      ret = unpacked
+    ret
+
+##=======================================================================


Add Librato configuration

Add Librato configuration

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

--- src/librato.coffee
+++ src/librato.coffee
@@ -0,0 +1,7 @@
+Client = require 'librato'
+
+module.exports = new Client \
+  process.env.LIBRATO_USER, \
+  process.env.LIBRATO_TOKEN, \
+  prefix: process.env.LIBRATO_PREFIX, source: process.env.DOTCLOUD_SERVICE_ID
+


Add my custom atom snippets

Add my custom atom snippets

error: pathspec '.atom/snippets.cson' did not match any file(s) known to git

--- .atom/snippets.cson
+++ .atom/snippets.cson
@@ -0,0 +1,143 @@
+# Your snippets
+#
+# Atom snippets allow you to enter a simple prefix in the editor and hit tab to
+# expand the prefix into a larger code block with templated values.
+#
+# You can create a new snippet in this file by typing "snip" and then hitting
+# tab.
+#
+# An example CoffeeScript snippet to expand log to console.log:
+#
+# '.source.coffee':
+#   'Console log':
+#     'prefix': 'log'
+#     'body': 'console.log $1'
+#
+# Each scope (e.g. '.source.coffee' above) can only be declared once.
+#
+# This file uses CoffeeScript Object Notation (CSON).
+# If you are unfamiliar with CSON, you can read more about it in the
+# Atom Flight Manual:
+# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
+
+'.source.js':
+  'Object Method':
+    'prefix': 'kf'
+    'body': '${1:methodName}: function (${2:attribute}) {\n\t$3\n}${4:,}'
+  'Object key — key: "value"':
+    'prefix': 'kv'
+    'body': '${1:key}: ${2:\'${3:value}\'}${4:, }'
+  'Prototype':
+    'prefix': 'proto'
+    'body': '${1:ClassName}.prototype.${2:methodName} = function ($3) {\n\t$0\n}'
+  'do':
+    'prefix': 'do'
+    'body': 'do {\n\t$2\n} while (${1:true})'
+  'condition ? true : false':
+    'prefix': 'tern'
+    'body': '${1:condition} ? ${2:true} : ${3:false}'
+  'if':
+    'prefix': 'if'
+    'body': 'if (${1:true}) {\n\t$2\n}'
+  'if … else':
+    'prefix': 'ife'
+    'body': 'if (${1:true}) {\n\t$2\n} else {\n\t$3\n}'
+  'else':
+    'prefix': 'else'
+    'body': 'else {\n\t$1\n}'
+  'else if':
+    'prefix': 'elseif'
+    'body': 'else if (${1:true}) {\n\t$2\n}'
+  'for':
+    'prefix' : 'for'
+    'body' : 'for (var ${2:i} = 0; ${2:i} < ${1:array}.length; ${2:i}++) {\n\t${1:array}[${2:i}]$3\n}'
+  'for in':
+    'prefix': 'forin'
+    'body': 'for (var ${1:variable} in ${2:object}) {\n\t${3:if (${2:object}.hasOwnProperty(${1:variable})) {\n\t\t$4\n\t\\}}\n}'
+  'for of':
+    'prefix': 'forof'
+    'body': 'for (${1:variable} of ${2:iterable}) {\n\t$3\n}'
+  'Function':
+    'prefix': 'fun'
+    'body': 'function ${1:functionName} ($2) {\n\t$0\n}'
+  'Anonymous Function':
+    'prefix': 'f'
+    'body': 'function ($1) {\n\t$2\n}'
+  'Arrow Function':
+    'prefix': 'af'
+    'body': '($1) => {\n\t$2\n}'
+  'Generator':
+    'prefix': 'gen',
+    'body': 'function* ${1:functionName}($2) {\n\t$0\n}'
+  'Anonymous Generator':
+    'prefix': 'g'
+    'body': 'function* ($1) {\n\t$2\n}'
+  'getElementsByClassName':
+    'prefix': 'get'
+    'body': 'getElementsByClassName(${1:\'${2:className}\'})$3'
+  'getElementsByName':
+    'prefix': 'getn'
+    'body': 'getElementsByName(${1:\'${2:name}\'})$3'
+  'getElementsByTagName':
+    'prefix': 'gett'
+    'body': 'getElementsByTagName(${1:\'${2:tagName}\'})$3'
+  'getElementById':
+    'prefix': 'geti'
+    'body': 'getElementById(${1:\'${2:id}\'})$3'
+  'querySelector':
+    'prefix': 'qs'
+    'body': 'querySelector(${1:\'${2:query}\'})$3'
+  'querySelectorAll':
+    'prefix': 'qsa'
+    'body': 'querySelectorAll(${1:\'${2:query}\'})$3'
+  'Immediately-Invoked Function Expression':
+    'prefix': 'iife'
+    'body': ';(function () {\n\t${1:\'use strict\'\n}\t$2\n}())'
+  'log':
+    'prefix': 'log'
+    'body': 'console.log($1)$0'
+  'warn':
+    'prefix': 'warn'
+    'body': 'console.warn($1)$0'
+  'error':
+    'prefix': 'error'
+    'body': 'console.error($1)$0'
+  'inspect':
+    'prefix': 'inspect'
+    'body': 'console.log(require(\'util\').inspect($0, { depth: null }))'
+  'new Promise':
+    'prefix': 'prom'
+    'body': 'new Promise(function (resolve, reject) {\n\t$1\n})$0'
+  'setInterval function':
+    'prefix': 'interval'
+    'body': 'setInterval(${2:function () {\n\t$3\n\\}}, ${1:10})'
+  'setTimeout function':
+    'prefix': 'timeout'
+    'body': 'setTimeout(${2:function () {\n\t$3\n\\}}, ${1:10})'
+  'switch':
+    'prefix': 'switch'
+    'body': 'switch (${1:expression}) {\n\tcase ${2:expression}:\n\t\t$4\n\t\tbreak$5\n\tdefault:\n\t\t$3\n}'
+  'case':
+    'prefix': 'case'
+    'body': 'case ${1:expression}:\n\t$2\n\tbreak'
+  'try':
+    'prefix': 'try'
+    'body': 'try {\n\t$1\n} catch (${2:e}) {\n\t$3\n}${4: finally {\n\t$5\n\\}}'
+  'while':
+    'prefix': 'while'
+    'body': 'while (${1:true}) {\n\t$2\n}'
+  'Start Docblock':
+    'prefix': '/**'
+    'body': '/**\n * $1\n */$0'
+  'CommonJS require':
+    'prefix': 'req'
+    'body': 'const ${1:module} = require(\'${1:module}\')'
+  'Class':
+    'prefix': 'class'
+    'body': 'class ${1:ClassName} {\n\tconstructor($2) {\n\t\t$3\n\t}\n}'
+  'export function':
+    'prefix': 'expfun'
+    'body': 'exports.${1:functionName} = function ($2) {\n\t${3:// body...}\n}'
+  'export module':
+    'prefix': 'expmod'
+    'body': 'module.exports = ${1:name}'


Fix bug with loading previous context.

Fix bug with loading previous context.

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/aha-app/bigquery-log-viewer.git/'


Test for new squad button.

Test for new squad button.

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

--- tests/test_new_squad.coffee
+++ tests/test_new_squad.coffee
@@ -0,0 +1,45 @@
+require = patchRequire global.require
+common = require './common'
+
+common.setup()
+
+
+casper.test.begin "New squad", (test) ->
+    common.waitForStartup('#rebel-builder')
+
+    common.createList('#rebel-builder', [
+        {
+            ship: 'X-Wing'
+            pilot: 'Rookie Pilot'
+            upgrades: [
+                null
+                'R2 Astromech'
+                null
+            ]
+        }
+        {
+            ship: 'A-Wing'
+            pilot: 'Prototype Pilot'
+            upgrades: [
+                'Concussion Missiles'
+                null
+            ]
+        }
+    ])
+
+    .then ->
+        @click '#rebel-builder .clear-squad'
+        @waitUntilVisible '.modal .modal-footer button.discard'
+    .then ->
+        @click '.modal .modal-footer button.btn-primary'
+    common.assertTotalPoints(test, '#rebel-builder', 43)
+
+    .then ->
+        @click '#rebel-builder .clear-squad'
+        @waitUntilVisible '.modal .modal-footer button.discard'
+    .then ->
+        @click '.modal .modal-footer button.discard'
+    common.assertTotalPoints(test, '#rebel-builder', 0)
+
+    .run ->
+        test.done()


fixed submitting shipments

fixed submitting shipments

--- app/assets/javascripts/trade/controllers/annual_report_upload_controller.js.coffee
+++ app/assets/javascripts/trade/controllers/annual_report_upload_controller.js.coffee
@@ -208,7 +208,9 @@
         .done( =>
           @set('sandboxShipmentsSubmitting', false)
         )
-        @transitionToRoute('shipments', {page: 1})
+        @transitionToRoute('shipments', {queryParams:
+          page: 1
+        })

     setFiltersFromErrorSelector: (errorSelector) ->
       @resetFilters()


add false argument to customTransitionToRoute function on aru controller

--- app/assets/javascripts/trade/controllers/annual_report_uploads_controller.js.coffee
+++ app/assets/javascripts/trade/controllers/annual_report_uploads_controller.js.coffee
@@ -14,10 +14,10 @@
   actions:
     transitionToReportUploadFromList: (aru)->
       aru.reload()
-      @customTransitionToRoute('annual_report_upload', aru)
+      @customTransitionToRoute('annual_report_upload', aru, false)

     transitionToReportUpload: (aru)->
-      @customTransitionToRoute('annual_report_upload', aru)
+      @customTransitionToRoute('annual_report_upload', aru, false)

     transitionToReportUploads: ()->
       @customTransitionToRoute('annual_report_uploads')


bugfix - do not reload iframe @TODO : some clean up on the whole hash management

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/clochix/cozy-home.git/'


Add global sentry/raven client

Add global sentry/raven client

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

--- coffee/db/sentry.coffee
+++ coffee/db/sentry.coffee
@@ -0,0 +1,15 @@
+Client = require('raven').Client
+
+Client.prototype.captureDocumentError = (req, err) ->
+  @captureMessage "#{req.type}: #{err.details[0].message}",
+    level: 'warning'
+    extra: req: req, err: err
+
+module.exports = new Client process.env.SENTRY_DNS
+
+module.exports.patchGlobal (id, err) ->
+  console.error 'Uncaught Exception'
+  console.error err.message
+  console.error err.stack
+  process.exit 1
+


Replace the quote

Replace the quote

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/Hiroto-K/HkApps.git/'


Add Spellbook.randomizer Component

Add Spellbook.randomizer Component

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

--- components/randomizer.coffee
+++ components/randomizer.coffee
@@ -0,0 +1,18 @@
+# *************************************
+#
+#   Randomize
+#   -> Randomly select a value
+#
+# *************************************
+
+Spellbook.randomizer = (collection) ->
+  randomNumber = Math.floor(Math.random() * collection.length)
+  return collection[randomNumber]
+
+# -------------------------------------
+#   Usage
+# -------------------------------------
+#
+# collection = ['one', 'two', 'three']
+# Spellbook.randomizer(collection)
+#


variable names changed again

variable names changed again

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/evercam/evercam-dashboard.git/'


BF-46 Save user settings

BF-46 Save user settings

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

--- app/assets/javascripts/model/user.coffee
+++ app/assets/javascripts/model/user.coffee
@@ -0,0 +1,64 @@
+### define
+libs/request : request
+###
+
+# This takes care of the userdate.
+User ?= {}
+
+
+User.Configuration =
+
+	#default userdata
+	moveValue : 1
+	rotateValue : 0.01
+	scaleValue : 0.05
+	mouseRotateValue : 0.004
+	mouseInversionX : 1
+	mouseInversionY : 1
+	mouseActive : true
+	keyboardActive : true
+	gamepadActive : false
+	motionsensorActive : false
+
+
+	initialize : ->
+		unless @configDeferred
+
+			@configDeferred = $.Deferred()
+
+			@configDeferred.fail =>
+				@configDeferred = null
+
+			request(url : '/user/configuration').then(
+
+				(data) =>
+					try
+						data = JSON.parse data
+
+
+					catch ex
+						@configDeferred.reject(ex)
+
+					@configDeferred.resolve(data)
+
+				(err) =>
+					@configDeferred.reject(err)
+			)
+
+			@configDeferred.promise()
+
+	push : ->
+		deferred = $.Deferred()
+
+		request(
+			url    : "/user/configuration"
+			method : 'POST'
+			contentType : "application/json"
+			data   : { mouseActive : "false", moveValue : "2" }
+		).fail( =>
+
+			console.log "could'nt save userdata"
+
+		).always(-> deferred.resolve())
+
+		deferred.promise()


small edit change

small edit change

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/floppyxyz/driver-manager.git/'


Added ability for stylefunctions being added to the map

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/MicrosmartSA/ubiquity-ui.git/'


Check if view is disabled in async handler

This prevents js errors since view.$el is empty if view was disposed

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/lookout/gringotts.git/'


Update basket item

Update basket item

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/Bogdan-p/bookingbug-angular.git/'


Begin of tests for Remote._

Begin of tests for Remote._

error: pathspec 'src/tests/spec/public/models/remote._.spec.coffee' did not match any file(s) known to git

--- src/tests/spec/public/models/remote._.spec.coffee
+++ src/tests/spec/public/models/remote._.spec.coffee
@@ -0,0 +1,161 @@
+require.config
+	paths:
+		'public': '../../public'
+
+require [
+	'public/models/remote._'
+	'public/models/message'
+	], ( Remote, Message ) ->
+
+	describe 'Remote', ->
+
+		remote = null
+		fakeController = null
+
+		beforeEach ->
+			fakeController = {
+				id: '1'
+				query: ->
+				relay: ->
+			}
+			spyOn(fakeController, 'query');
+			spyOn(fakeController, 'relay');
+			spyOn(Remote.prototype, 'initialize');
+			spyOn(Remote.prototype, '_send');
+
+			Remote.hashes = []
+
+			remote = new Remote(fakeController, 1, 2)
+
+		describe 'when constructed', ->
+
+			it 'should have the given controller', ->
+				expect(remote._controller).toEqual(fakeController)
+
+			it 'should have called @initialize with the constructor arguments', ->
+				expect(remote.initialize).toHaveBeenCalledWith(1, 2)
+
+		describe 'when killed', ->
+
+			it 'should remove all listeners', ->
+				spyOn(remote, 'off')
+				spyOn(remote, 'isConnected').andReturn(false)
+				spyOn(remote, 'disconnect')
+				remote.die()
+				expect(remote.off).toHaveBeenCalled()
+				expect(remote.disconnect).not.toHaveBeenCalled()
+
+			it 'should disconnect if there is a connection', ->
+				spyOn(remote, 'isConnected').andReturn(true)
+				spyOn(remote, 'disconnect')
+				remote.die()
+				expect(remote.disconnect).toHaveBeenCalled()
+
+		describe 'when receiving a message', ->
+
+			it 'should discard the same message coming in', ->
+				to = fakeController.id
+				from = fakeController.id + '1'
+				message = new Message(to, from, 'testEvent')
+
+				# Trigger twice, hash is the same, so should only be processed once
+				expect(Remote.hashes.length).toBe(0)
+				remote.trigger('message', message.serialize())
+				remote.trigger('message', message.serialize())
+				expect(Remote.hashes.length).toBe(1)
+
+			it 'should limit the hashtable size to 1000', ->
+				to = fakeController.id
+				from = fakeController.id + '1'
+
+				expect(Remote.hashes.length).toBe(0)
+				for i in [0...1000]
+					message = new Message(to, from, 'testEvent', i)
+
+					# Trigger twice, hash is the same, so should only be processed once
+					remote.trigger('message', message.serialize())
+
+				expect(Remote.hashes.length).toBe(1000)
+
+				# The next message should splice of 200 hashes and add this new one
+				message = new Message(to, from, 'testEvent', 1000)
+				remote.trigger('message', message.serialize())
+				expect(Remote.hashes.length).toBe(801)
+
+			it 'should trigger the event from the message on our instance if `to` is our controller', ->
+				to = fakeController.id
+				from = fakeController.id + '1'
+				message = new Message(to, from, 'testEvent', 1000)
+
+				remote.on('testEvent', ( number ) =>
+						expect(number).toBe(1000)
+					)
+				remote.trigger('message', message.serialize())
+
+			it 'should relay messages that are not for our controller', ->
+				to = fakeController.id + '1'
+				from = fakeController.id
+				message = new Message(to, from, 'testEvent', 1000)
+
+				remote.on('testEvent', ( number ) =>
+						throw "Should not parse this event"
+					)
+				remote.trigger('message', message.serialize())
+
+				expect(fakeController.relay).toHaveBeenCalledWith(message)
+
+			it 'should relay and parse messages that for `*`', ->
+				to = '*'
+				from = fakeController.id
+				message = new Message(to, from, 'testEvent', 1000)
+
+				remote.on('testEvent', ( number ) =>
+						expect(number).toBe(1000)
+					)
+				remote.trigger('message', message.serialize())
+
+				expect(fakeController.relay).toHaveBeenCalledWith(message)
+
+		describe 'when emitting', ->
+
+			it 'should create a message and send it', ->
+				spyOn(remote, 'send')
+
+				remote.emit('testEvent', 1)
+
+				expect(remote.send).toHaveBeenCalled()
+
+				# Check if argument is correct
+				argument = remote.send.mostRecentCall.args[0]
+				expect(argument instanceof Message).toBe(true)
+				expect(argument.event).toBe('testEvent')
+				expect(argument.args.length).toBe(1)
+				expect(argument.args[0]).toBe(1)
+
+			it 'should create a message with a specified receiver and send it', ->
+				spyOn(remote, 'send')
+
+				remote.emitTo('123', 'testEvent', 1)
+
+				expect(remote.send).toHaveBeenCalled()
+
+				# Check if argument is correct
+				argument = remote.send.mostRecentCall.args[0]
+				expect(argument instanceof Message).toBe(true)
+				expect(argument.event).toBe('testEvent')
+				expect(argument.args.length).toBe(1)
+				expect(argument.args[0]).toBe(1)
+				expect(argument.to).toBe('123')
+
+		describe 'when sending', ->
+
+			it 'should hash the message and send it', ->
+				to = fakeController.id
+				from = fakeController.id + '1'
+				message = new Message(to, from, 'testEvent')
+
+				expect(Remote.hashes.length).toBe(0)
+				remote.send(message)
+				remote.send(message)
+				expect(Remote.hashes.length).toBe(1)
+				expect(remote._send.callCount).toBe(2)


Update core.Repository with updated websocket functionality


tests template for zero state

tests template for zero state

--- apps/artist_new/test/templates.coffee
+++ apps/artist_new/test/templates.coffee
@@ -1,6 +1,7 @@
 jade = require 'jade'
 fs = require 'fs'
 _s = require 'underscore.string'
+_ = require 'underscore'
 benv = require 'benv'
 { resolve } = require 'path'
 sinon = require 'sinon'
@@ -45,6 +46,7 @@
         params: new Backbone.Model
         filterLabelMap: require '../../../components/filter2/dropdown/label_map.coffee'
         _s: _s
+        _: _
       }, done

     it 'should not display the no works message if there is more than 0 artworks', ->
@@ -60,7 +62,7 @@
       $navLinks.first().text().should.equal 'Overview'
       $navLinks.last().text().should.equal 'Related Artists'

-  describe 'artist with some artworks (on the overview page)', ->
+  describe 'artist with 0 artworks (on the overview page)', ->
     beforeEach (done) ->
       @artist = new Artist fabricate 'artist', published_artworks_count: 0
       @carousel = new Carousel artist: @artist
@@ -71,7 +73,10 @@
         artists: false
         contemporary: false

-      @filterArtworks = new FilterArtworks fabricate2('filter_artworks'), { parse: true }
+      artworks = _.clone(fabricate2('filter_artworks'))
+      artworks.aggregations.total.value = 0
+
+      @filterArtworks = new FilterArtworks artworks, { parse: true }

       benv.render resolve(__dirname, '../templates/index.jade'), {
         sd: CURRENT_PATH: "/artist/#{@artist.id}/shows"
@@ -85,10 +90,11 @@
         params: new Backbone.Model
         filterLabelMap: require '../../../components/filter2/dropdown/label_map.coffee'
         _s: _s
+        _: _
       }, done

     # pending until there is an empty state
-    xit 'should display the no works message if there is 0 artworks', ->
+    it 'should display the no works message if there is 0 artworks', ->
       @artist.get('published_artworks_count').should.equal 0
       $('body').html().should.containEql "There are no #{@artist.get('name')} works on Artsy yet."

@@ -124,6 +130,7 @@
         params: new Backbone.Model
         filterLabelMap: require '../../../components/filter2/dropdown/label_map.coffee'
         _s: _s
+        _: _
       }, done

     it 'displays a link to the auction results', ->


Invoke callback when no work to be done

Invoke callback when no work to be done

--- tasks/update-atom-shell-task.coffee
+++ tasks/update-atom-shell-task.coffee
@@ -85,6 +85,8 @@
                   rebuildNativeModules(currentAtomShellVersion, done)
             else
               done(false)
+      else
+        done()
     else
       grunt.log.error("atom-shell version missing from package.json")
       done(false)


Fixed Layers Menu populating incorrectly after loading many scenarios

--- src/main/webapp/lib/views/LayersMenuView.coffee
+++ src/main/webapp/lib/views/LayersMenuView.coffee
@@ -21,11 +21,15 @@
     @$el.attr 'id', @options.id if @options.id
     @render()
     _.each(@menuItems, (item) => new $a.LayersMenuViewItem(@id, item))
+    $a.broker.on("map:clear_map",@_clear, @)

   render: ->
     $("##{@options.parentId}").append(@el)
     @

+  _clear: ->
+    $("##{@id}").remove()
+
   #these open and close the Layers Menu itself
   displayOff: (e) ->
     @$el.removeClass "open"


when starring a project it correctly increase the star count

This is instead of increasing star & fork count

Fixed #11396

--- app/assets/javascripts/star.js.coffee
+++ app/assets/javascripts/star.js.coffee
@@ -6,7 +6,7 @@
       $starIcon = $this.find('i')

       toggleStar = (isStarred) ->
-        $this.parent().find('span.count').text data.star_count
+        $this.parent().find('.star-count').text data.star_count
         if isStarred
           $starSpan.removeClass('starred').text 'Star'
           $starIcon.removeClass('fa-star').addClass 'fa-star-o'
@@ -19,4 +19,4 @@
       return
     ).on 'ajax:error', (e, xhr, status, error) ->
       new Flash('Star toggle failed. Try again later.', 'alert')
-      return+      return


Nginx: handle false state correctly for disableLocation

--- deployment/nginx.coffee
+++ deployment/nginx.coffee
@@ -143,9 +143,9 @@
   for name, options of workers
     # don't add those who whish not to be generated, probably because those are
     # using manually written locations
-    continue unless options.nginx?.locations
-
-    continue if options.nginx?.disableLocation?
+    continue  unless options.nginx?.locations
+
+    continue  if options.nginx?.disableLocation

     # some of the locations can be limited to some environments, while creating
     # nginx locations filter with this info


small fix

small fix

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/NoelLynch/bigthunkjs.git/'


fix: fix run-confirm template

fix: fix run-confirm template

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/keboola/kbc-ui-ex-gdrive.git/'


Update private directory

Update private directory

--- helper/settings.coffee
+++ helper/settings.coffee
@@ -13,7 +13,7 @@
     "#{splinters.dir}/#{splinters.name}#{splinters.ext}" # file itself
     "#{splinters.dir}/#{splinters.name}.specific#{splinters.ext}"
     "#{splinters.dir}/#{splinters.name}.local#{splinters.ext}"
-    "#{process.env[if process.platform is "win32" then "USERPROFILE" else "HOME"]}/.fixin/settings/#{splinters.name}.private#{splinters.ext}"
+    "#{process.env[if process.platform is "win32" then "USERPROFILE" else "HOME"]}/.spirehq/settings/#{splinters.name}.private#{splinters.ext}"
   ]
   for file in files
     if fs.existsSync(file)


Add AsyncFs module

Add AsyncFs module

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

--- src/asyncfs.coffee
+++ src/asyncfs.coffee
@@ -0,0 +1,195 @@
+# Pre dependencies
+UnJSONable = require("./unjsonable")
+
+###*
+@class AsyncFs
+  Asynchronous file system like fs module in Node.js
+@extends Rubic.UnJSONable
+###
+class AsyncFs extends UnJSONable
+  null
+
+  invokeCallback = (callback, promise) ->
+    promise.then((value...) ->
+      callback(null, value...)
+      return
+    ).catch((error) ->
+      callback(error)
+      return
+    )
+    return
+
+  #--------------------------------------------------------------------------------
+  # Node.js compatible methods
+  #
+
+  ###*
+  @method
+    Make a new directory
+  @param {string} path
+    Directory path
+  @param {number} [mode]
+    Mode (permissions)
+  @param {function(Error/null):undefined} [callback]
+    Callback function when Promise is not used
+  @return {undefined/Promise}
+    Promise object when callback is omitted
+  ###
+  mkdir: (path, mode = 0o777, callback) ->
+    if typeof(mode) == "function"
+      callback = mode
+      mode = 0o777
+    return invokeCallback(callback, @mkdir(path, mode)) if callback?
+    return @mkdirImpl(path, mode)
+
+  ###*
+  @method
+    Read contents from file
+  @param {string} file
+    File path
+  @param {Object/string} [options]
+    Options
+  @param {function(Error/null,Object):undefined} [callback]
+    Callback function when Promise is not used
+  @return {undefined/Promise}
+    Promise object when callback is omitted
+  ###
+  readFile: (file, options = {flag: "r"}, callback) ->
+    if typeof(options) == "function"
+      callback = options
+      options = null
+    return invokeCallback(callback, @readFile(file, options)) if callback?
+    options = {encoding: options} if typeof(options) == "string"
+    return @readFileImpl(file, options)
+
+  ###*
+  @method
+    Write contents to file
+  @param {string} file
+    File path
+  @param {Object/string} [options]
+    Options
+  @param {function(Error/null):undefined} [callback]
+    Callback function when Promise is not used
+  @return {undefined/Promise}
+    Promise object when callback is omitted
+  ###
+  writeFile: (file, data, options = {mode: 0o666, flag: "w"}, callback) ->
+    if typeof(options) == "function"
+      callback = options
+      options = null
+    return invokeCallback(callback, @writeFile(file, data, options)) if callback?
+    options = {encoding: options} if typeof(options) == "string"
+    return @writeFileImpl(file, data, options)
+
+  ###*
+  @method
+    Unlink file
+  @param {string} path
+    File path
+  @param {function(Error/null):undefined} [callback]
+    Callback function when Promise is not used
+  @return {undefined/Promise}
+    Promise object when callback is omitted
+  ###
+  unlink: (path, callback) ->
+    return invokeCallback(callback, @unlink(path)) if callback?
+    return @unlinkImpl(path)
+
+  #--------------------------------------------------------------------------------
+  # Original methods
+  #
+
+  ###*
+  @method
+    Open directory as fs object
+  @param {string} path
+    File path
+  @param {function(Error/null,Rubic.AsyncFs):undefined} [callback]
+    Callback function when Promise is not used
+  @return {undefined/Promise}
+    Promise object when callback is omitted
+  @return {Rubic.AsyncFs} return.PromiseValue
+    New fs object for directory
+  ###
+  opendirfs: (path, callback) ->
+    return invokeCallback(callback, @opendirfs(path)) if callback?
+    return @opendirfsImpl(path)
+
+  #--------------------------------------------------------------------------------
+  # Protected methods
+  #
+
+  ###*
+  @protected
+  @method
+    Implement of mkdir method
+  @param {string} path
+    Directory path
+  @param {number} mode
+    Mode (permissions)
+  @return {Promise}
+    Promise object
+  ###
+  mkdirImpl: (path, mode) ->
+    return Promise.reject(Error("Not supported"))
+
+  ###*
+  @protected
+  @method
+    Implement of readFile method
+  @param {string} file
+    File path
+  @param {Object} options
+    Options
+  @return {Promise}
+    Promise object
+  @return {Object} return.PromiseValue
+    Read data
+  ###
+  readFileImpl: (file, options) ->
+    return Promise.reject(Error("Not supported"))
+
+  ###*
+  @protected
+  @method
+    Implement of writeFile method
+  @param {string} file
+    File path
+  @param {Object} data
+    Write data
+  @param {Object} options
+    Options
+  @return {Promise}
+    Promise object
+  ###
+  writeFileImpl: (file, data, options) ->
+    return Promise.reject(Error("Not supported"))
+
+  ###*
+  @protected
+  @method
+    Implement of unlink method
+  @param {string} path
+    File path
+  @return {Promise}
+    Promise object
+  ###
+  unlinkImpl: (path) ->
+    return Promise.reject(Error("Not supported"))
+
+  ###*
+  @protected
+  @method
+    Implement of opendirfs method
+  @param {string} path
+    File path
+  @return {Promise}
+    Promise object
+  @return {Rubic.AsyncFs} return.PromiseValue
+    New fs object for directory
+  ###
+  opendirfsImpl: (path) ->
+    return Promise.reject(Error("Not supported"))
+
+module.exports = AsyncFs