pageCache = {} cacheSize = 10 transitionCacheEnabled = false requestCachingEnabled = true progressBar = null progressBarDelay = 400

currentState = null loadedAssets = null

referer = null

xhr = null

EVENTS =

BEFORE_CHANGE:  'page:before-change'
FETCH:          'page:fetch'
RECEIVE:        'page:receive'
CHANGE:         'page:change'
UPDATE:         'page:update'
LOAD:           'page:load'
PARTIAL_LOAD:   'page:partial-load'
RESTORE:        'page:restore'
BEFORE_UNLOAD:  'page:before-unload'
AFTER_REMOVE:   'page:after-remove'

isPartialReplacement = (options) ->

options.change or options.append or options.prepend

fetch = (url, options = {}) ->

url = new ComponentUrl url

return if pageChangePrevented(url.absolute)

if url.crossOrigin()
  document.location.href = url.absolute
  return

if isPartialReplacement(options) or options.keep
  removeCurrentPageFromCache()
else
  cacheCurrentPage()

rememberReferer()
progressBar?.start(delay: progressBarDelay)

if transitionCacheEnabled and !isPartialReplacement(options) and cachedPage = transitionCacheFor(url.absolute)
  reflectNewUrl(url)
  fetchHistory cachedPage
  options.showProgressBar = false
  options.scroll = false
else
  options.scroll ?= false if isPartialReplacement(options) and !url.hash

fetchReplacement url, options

transitionCacheFor = (url) ->

return if url is currentState.url
cachedPage = pageCache[url]
cachedPage if cachedPage and !cachedPage.transitionCacheDisabled

enableTransitionCache = (enable = true) ->

transitionCacheEnabled = enable

disableRequestCaching = (disable = true) ->

requestCachingEnabled = not disable
disable

fetchReplacement = (url, options) ->

options.cacheRequest ?= requestCachingEnabled
options.showProgressBar ?= true

triggerEvent EVENTS.FETCH, url: url.absolute

xhr?.abort()
xhr = new XMLHttpRequest
xhr.open 'GET', url.formatForXHR(cache: options.cacheRequest), true
xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
xhr.setRequestHeader 'X-XHR-Referer', referer

xhr.onload = ->
  triggerEvent EVENTS.RECEIVE, url: url.absolute

  if doc = processResponse()
    reflectNewUrl url
    reflectRedirectedUrl()
    loadedNodes = changePage extractTitleAndBody(doc)..., options
    if options.showProgressBar
      progressBar?.done()
    updateScrollPosition(options.scroll)
    triggerEvent (if isPartialReplacement(options) then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes
    constrainPageCacheTo(cacheSize)
  else
    progressBar?.done()
    document.location.href = crossOriginRedirect() or url.absolute

if progressBar and options.showProgressBar
  xhr.onprogress = (event) =>
    percent = if event.lengthComputable
      event.loaded / event.total * 100
    else
      progressBar.value + (100 - progressBar.value) / 10
    progressBar.advanceTo(percent)

xhr.onloadend = -> xhr = null
xhr.onerror   = -> document.location.href = url.absolute

xhr.send()

fetchHistory = (cachedPage, options = {}) ->

xhr?.abort()
changePage cachedPage.title, cachedPage.body, null, runScripts: false
progressBar?.done()
updateScrollPosition(options.scroll)
triggerEvent EVENTS.RESTORE

cacheCurrentPage = ->

currentStateUrl = new ComponentUrl currentState.url

pageCache[currentStateUrl.absolute] =
  url:                      currentStateUrl.relative,
  body:                     document.body,
  title:                    document.title,
  positionY:                window.pageYOffset,
  positionX:                window.pageXOffset,
  cachedAt:                 new Date().getTime(),
  transitionCacheDisabled:  document.querySelector('[data-no-transition-cache]')?

removeCurrentPageFromCache = ->

delete pageCache[new ComponentUrl(currentState.url).absolute]

pagesCached = (size = cacheSize) ->

cacheSize = parseInt(size) if /^[\d]+$/.test size

constrainPageCacheTo = (limit) ->

pageCacheKeys = Object.keys pageCache

cacheTimesRecentFirst = pageCacheKeys.map (url) ->
  pageCache[url].cachedAt
.sort (a, b) -> b - a

for key in pageCacheKeys when pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]
  onNodeRemoved(pageCache[key].body)
  delete pageCache[key]

replace = (html, options = {}) ->

loadedNodes = changePage extractTitleAndBody(createDocument(html))..., options
triggerEvent (if isPartialReplacement(options) then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes

changePage = (title, body, csrfToken, options) ->

title = options.title ? title
currentBody = document.body

if isPartialReplacement(options)
  nodesToAppend = findNodesMatchingKeys(currentBody, options.append) if options.append
  nodesToPrepend = findNodesMatchingKeys(currentBody, options.prepend) if options.prepend

  nodesToReplace = findNodes(currentBody, '[data-turbolinks-temporary]')
  nodesToReplace = nodesToReplace.concat findNodesMatchingKeys(currentBody, options.change) if options.change

  nodesToChange = [].concat(nodesToAppend || [], nodesToPrepend || [], nodesToReplace || [])
  nodesToChange = removeDuplicates(nodesToChange)
else
  nodesToChange = [currentBody]

triggerEvent EVENTS.BEFORE_UNLOAD, nodesToChange
document.title = title if title isnt false

if isPartialReplacement(options)
  appendedNodes = swapNodes(body, nodesToAppend, keep: false, append: true) if nodesToAppend
  prependedNodes = swapNodes(body, nodesToPrepend, keep: false, prepend: true) if nodesToPrepend
  replacedNodes = swapNodes(body, nodesToReplace, keep: false) if nodesToReplace

  changedNodes = [].concat(appendedNodes || [], prependedNodes || [], replacedNodes || [])
  changedNodes = removeDuplicates(changedNodes)
else
  unless options.flush
    nodesToKeep = findNodes(currentBody, '[data-turbolinks-permanent]')
    nodesToKeep.push(findNodesMatchingKeys(currentBody, options.keep)...) if options.keep
    swapNodes(body, removeDuplicates(nodesToKeep), keep: true)

  document.body = body
  CSRFToken.update csrfToken if csrfToken?
  setAutofocusElement()
  changedNodes = [body]

executeScriptTags(getScriptsToRun(changedNodes, options.runScripts))
currentState = window.history.state

triggerEvent EVENTS.CHANGE, changedNodes
triggerEvent EVENTS.UPDATE
return changedNodes

findNodes = (body, selector) ->

Array::slice.apply(body.querySelectorAll(selector))

findNodesMatchingKeys = (body, keys) ->

matchingNodes = []
for key in (if Array.isArray(keys) then keys else [keys])
  matchingNodes.push(findNodes(body, '[id^="'+key+':"], [id="'+key+'"]')...)

return matchingNodes

swapNodes = (targetBody, existingNodes, options) ->

changedNodes = []
for existingNode in existingNodes
  unless nodeId = existingNode.getAttribute('id')
    throw new Error("Turbolinks partial replace: turbolinks elements must have an id.")

  if targetNode = targetBody.querySelector('[id="'+nodeId+'"]')
    if options.keep
      existingNode.parentNode.insertBefore(existingNode.cloneNode(true), existingNode)
      existingNode = targetNode.ownerDocument.adoptNode(existingNode)
      targetNode.parentNode.replaceChild(existingNode, targetNode)
    else
      if options.append or options.prepend
        firstChild = existingNode.firstChild

        childNodes = Array::slice.call targetNode.childNodes, 0 # a copy has to be made since the list is mutated while processing

        for childNode in childNodes
          if !firstChild or options.append # when the parent node is empty, there is no difference between appending and prepending
            existingNode.appendChild(childNode)
          else if options.prepend
            existingNode.insertBefore(childNode, firstChild)

        changedNodes.push(existingNode)
      else
        existingNode.parentNode.replaceChild(targetNode, existingNode)
        onNodeRemoved(existingNode)
        changedNodes.push(targetNode)

return changedNodes

onNodeRemoved = (node) ->

if typeof jQuery isnt 'undefined'
  jQuery(node).remove()
triggerEvent(EVENTS.AFTER_REMOVE, node)

getScriptsToRun = (changedNodes, runScripts) ->

selector = if runScripts is false then 'script[data-turbolinks-eval="always"]' else 'script:not([data-turbolinks-eval="false"])'
script for script in document.body.querySelectorAll(selector) when isEvalAlways(script) or (nestedWithinNodeList(changedNodes, script) and not withinPermanent(script))

isEvalAlways = (script) ->

script.getAttribute('data-turbolinks-eval') is 'always'

withinPermanent = (element) ->

while element?
  return true if element.hasAttribute?('data-turbolinks-permanent')
  element = element.parentNode

return false

nestedWithinNodeList = (nodeList, element) ->

while element?
  return true if element in nodeList
  element = element.parentNode

return false

executeScriptTags = (scripts) ->

for script in scripts when script.type in ['', 'text/javascript']
  copy = document.createElement 'script'
  copy.setAttribute attr.name, attr.value for attr in script.attributes
  copy.async = false unless script.hasAttribute 'async'
  copy.appendChild document.createTextNode script.innerHTML
  { parentNode, nextSibling } = script
  parentNode.removeChild script
  parentNode.insertBefore copy, nextSibling
return

# Firefox bug: Doesn't autofocus fields that are inserted via JavaScript setAutofocusElement = ->

autofocusElement = (list = document.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1]
if autofocusElement and document.activeElement isnt autofocusElement
  autofocusElement.focus()

reflectNewUrl = (url) ->

if (url = new ComponentUrl url).absolute not in [referer, document.location.href]
  window.history.pushState { turbolinks: true, url: url.absolute }, '', url.absolute

reflectRedirectedUrl = ->

if location = xhr.getResponseHeader 'X-XHR-Redirected-To'
  location = new ComponentUrl location
  preservedHash = if location.hasNoHash() then document.location.hash else ''
  window.history.replaceState window.history.state, '', location.href + preservedHash

crossOriginRedirect = ->

redirect if (redirect = xhr.getResponseHeader('Location'))? and (new ComponentUrl(redirect)).crossOrigin()

rememberReferer = ->

referer = document.location.href

rememberCurrentUrlAndState = ->

window.history.replaceState { turbolinks: true, url: document.location.href }, '', document.location.href
currentState = window.history.state

updateScrollPosition = (position) ->

if Array.isArray(position)
  window.scrollTo position[0], position[1]
else if position isnt false
  if document.location.hash
    document.location.href = document.location.href
    rememberCurrentUrlAndState()
  else
    window.scrollTo 0, 0

clone = (original) ->

return original if not original? or typeof original isnt 'object'
copy = new original.constructor()
copy[key] = clone value for key, value of original
copy

removeDuplicates = (array) ->

result = []
result.push(obj) for obj in array when result.indexOf(obj) is -1
result

popCookie = (name) ->

value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
value

uniqueId = ->

new Date().getTime().toString(36)

triggerEvent = (name, data) ->

if typeof Prototype isnt 'undefined'
  Event.fire document, name, data, true

event = document.createEvent 'Events'
event.data = data if data
event.initEvent name, true, true
document.dispatchEvent event

pageChangePrevented = (url) ->

!triggerEvent EVENTS.BEFORE_CHANGE, url: url

processResponse = ->

clientOrServerError = ->
  400 <= xhr.status < 600

validContent = ->
  (contentType = xhr.getResponseHeader('Content-Type'))? and
    contentType.match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/

downloadingFile = ->
  (disposition = xhr.getResponseHeader('Content-Disposition'))? and
    disposition.match /^attachment/

extractTrackAssets = (doc) ->
  for node in doc.querySelector('head').childNodes when node.getAttribute?('data-turbolinks-track')?
    node.getAttribute('src') or node.getAttribute('href')

assetsChanged = (doc) ->
  loadedAssets ||= extractTrackAssets document
  fetchedAssets  = extractTrackAssets doc
  fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length

intersection = (a, b) ->
  [a, b] = [b, a] if a.length > b.length
  value for value in a when value in b

if not clientOrServerError() and validContent() and not downloadingFile()
  doc = createDocument xhr.responseText
  if doc and !assetsChanged doc
    return doc

extractTitleAndBody = (doc) ->

title = doc.querySelector 'title'
[ title?.textContent, doc.querySelector('body'), CSRFToken.get(doc).token ]

CSRFToken =

get: (doc = document) ->
  node:   tag = doc.querySelector 'meta[name="csrf-token"]'
  token:  tag?.getAttribute? 'content'

update: (latest) ->
  current = @get()
  if current.token? and latest? and current.token isnt latest
    current.node.setAttribute 'content', latest

createDocument = (html) ->

if /<(html|body)/i.test(html)
  doc = document.documentElement.cloneNode()
  doc.innerHTML = html
else
  doc = document.documentElement.cloneNode(true)
  doc.querySelector('body').innerHTML = html
doc.head = doc.querySelector('head')
doc.body = doc.querySelector('body')
doc

# The ComponentUrl class converts a basic URL string into an object # that behaves similarly to document.location. # # If an instance is created from a relative URL, the current document # is used to fill in the missing attributes (protocol, host, port). class ComponentUrl

constructor: (@original = document.location.href) ->
  return @original if @original.constructor is ComponentUrl
  @_parse()

withoutHash: -> @href.replace(@hash, '').replace('#', '')

# Intention revealing function alias
withoutHashForIE10compatibility: -> @withoutHash()

hasNoHash: -> @hash.length is 0

crossOrigin: ->
  @origin isnt (new ComponentUrl).origin

formatForXHR: (options = {}) ->
  (if options.cache then @ else @withAntiCacheParam()).withoutHashForIE10compatibility()

withAntiCacheParam: ->
  new ComponentUrl(
    if /([?&])_=[^&]*/.test @absolute
      @absolute.replace /([?&])_=[^&]*/, "$1_=#{uniqueId()}"
    else
      new ComponentUrl(@absolute + (if /\?/.test(@absolute) then "&" else "?") + "_=#{uniqueId()}")
  )

_parse: ->
  (@link ?= document.createElement 'a').href = @original
  { @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link
  @origin = [@protocol, '//', @hostname].join ''
  @origin += ":#{@port}" unless @port.length is 0
  @relative = [@pathname, @search, @hash].join ''
  @absolute = @href

# The Link class derives from the ComponentUrl class, but is built from an # existing link element. Provides verification functionality for Turbolinks # to use in determining whether it should process the link when clicked. class Link extends ComponentUrl

@HTML_EXTENSIONS: ['html']

@allowExtensions: (extensions...) ->
  Link.HTML_EXTENSIONS.push extension for extension in extensions
  Link.HTML_EXTENSIONS

constructor: (@link) ->
  return @link if @link.constructor is Link
  @original = @link.href
  @originalElement = @link
  @link = @link.cloneNode false
  super

shouldIgnore: ->
  @crossOrigin() or
    @_anchored() or
    @_nonHtml() or
    @_optOut() or
    @_target()

_anchored: ->
  (@hash.length > 0 or @href.charAt(@href.length - 1) is '#') and
    (@withoutHash() is (new ComponentUrl).withoutHash())

_nonHtml: ->
  @pathname.match(/\.[a-z]+$/g) and not @pathname.match(new RegExp("\\.(?:#{Link.HTML_EXTENSIONS.join('|')})?$", 'g'))

_optOut: ->
  link = @originalElement
  until ignore or link is document
    ignore = link.getAttribute('data-no-turbolink')?
    link = link.parentNode
  ignore

_target: ->
  @link.target.length isnt 0

# The Click class handles clicked links, verifying if Turbolinks should # take control by inspecting both the event and the link. If it should, # the page change process is initiated. If not, control is passed back # to the browser for default functionality. class Click

@installHandlerLast: (event) ->
  unless event.defaultPrevented
    document.removeEventListener 'click', Click.handle, false
    document.addEventListener 'click', Click.handle, false

@handle: (event) ->
  new Click event

constructor: (@event) ->
  return if @event.defaultPrevented
  @_extractLink()
  if @_validForTurbolinks()
    visit @link.href
    @event.preventDefault()

_extractLink: ->
  link = @event.target
  link = link.parentNode until !link.parentNode or link.nodeName is 'A'
  @link = new Link(link) if link.nodeName is 'A' and link.href.length isnt 0

_validForTurbolinks: ->
  @link? and not (@link.shouldIgnore() or @_nonStandardClick())

_nonStandardClick: ->
  @event.which > 1 or
    @event.metaKey or
    @event.ctrlKey or
    @event.shiftKey or
    @event.altKey

class ProgressBar

className = 'turbolinks-progress-bar'
# Setting the opacity to a value < 1 fixes a display issue in Safari 6 and
# iOS 6 where the progress bar would fill the entire page.
originalOpacity = 0.99

@enable: ->
  progressBar ?= new ProgressBar 'html'

@disable: ->
  progressBar?.uninstall()
  progressBar = null

constructor: (@elementSelector) ->
  @value = 0
  @content = ''
  @speed = 300
  @opacity = originalOpacity
  @install()

install: ->
  @element = document.querySelector(@elementSelector)
  @element.classList.add(className)
  @styleElement = document.createElement('style')
  document.head.appendChild(@styleElement)
  @_updateStyle()

uninstall: ->
  @element.classList.remove(className)
  document.head.removeChild(@styleElement)

start: ({delay} = {})->
  clearTimeout(@displayTimeout)
  if delay
    @display = false
    @displayTimeout = setTimeout =>
      @display = true
    , delay
  else
    @display = true

  if @value > 0
    @_reset()
    @_reflow()

  @advanceTo(5)

advanceTo: (value) ->
  if value > @value <= 100
    @value = value
    @_updateStyle()

    if @value is 100
      @_stopTrickle()
    else if @value > 0
      @_startTrickle()

done: ->
  if @value > 0
    @advanceTo(100)
    @_finish()

_finish: ->
  @fadeTimer = setTimeout =>
    @opacity = 0
    @_updateStyle()
  , @speed / 2

  @resetTimer = setTimeout(@_reset, @speed)

_reflow: ->
  @element.offsetHeight

_reset: =>
  @_stopTimers()
  @value = 0
  @opacity = originalOpacity
  @_withSpeed(0, => @_updateStyle(true))

_stopTimers: ->
  @_stopTrickle()
  clearTimeout(@fadeTimer)
  clearTimeout(@resetTimer)

_startTrickle: ->
  return if @trickleTimer
  @trickleTimer = setTimeout(@_trickle, @speed)

_stopTrickle: ->
  clearTimeout(@trickleTimer)
  delete @trickleTimer

_trickle: =>
  @advanceTo(@value + Math.random() / 2)
  @trickleTimer = setTimeout(@_trickle, @speed)

_withSpeed: (speed, fn) ->
  originalSpeed = @speed
  @speed = speed
  result = fn()
  @speed = originalSpeed
  result

_updateStyle: (forceRepaint = false) ->
  @_changeContentToForceRepaint() if forceRepaint
  @styleElement.textContent = @_createCSSRule()

_changeContentToForceRepaint: ->
  @content = if @content is '' then ' ' else ''

_createCSSRule: ->
  """
  #{@elementSelector}.#{className}::before {
    content: '#{@content}';
    position: fixed;
    top: 0;
    left: 0;
    z-index: 2000;
    background-color: #0076ff;
    height: 3px;
    opacity: #{@opacity};
    width: #{if @display then @value else 0}%;
    transition: width #{@speed}ms ease-out, opacity #{@speed / 2}ms ease-in;
    transform: translate3d(0,0,0);
  }
  """

ProgressBarAPI =

enable: ProgressBar.enable
disable: ProgressBar.disable
setDelay: (value) -> progressBarDelay = value
start: (options) -> ProgressBar.enable().start(options)
advanceTo: (value) -> progressBar?.advanceTo(value)
done: -> progressBar?.done()

installDocumentReadyPageEventTriggers = ->

document.addEventListener 'DOMContentLoaded', ( ->
  triggerEvent EVENTS.CHANGE, [document.body]
  triggerEvent EVENTS.UPDATE
), true

installJqueryAjaxSuccessPageUpdateTrigger = ->

if typeof jQuery isnt 'undefined'
  jQuery(document).on 'ajaxSuccess', (event, xhr, settings) ->
    return unless jQuery.trim xhr.responseText
    triggerEvent EVENTS.UPDATE

onHistoryChange = (event) ->

if event.state?.turbolinks && event.state.url != currentState.url
  previousUrl = new ComponentUrl(currentState.url)
  newUrl = new ComponentUrl(event.state.url)

  if newUrl.withoutHash() is previousUrl.withoutHash()
    updateScrollPosition()
  else if cachedPage = pageCache[newUrl.absolute]
    cacheCurrentPage()
    fetchHistory cachedPage, scroll: [cachedPage.positionX, cachedPage.positionY]
  else
    visit event.target.location.href

initializeTurbolinks = ->

rememberCurrentUrlAndState()
ProgressBar.enable()

document.addEventListener 'click', Click.installHandlerLast, true
window.addEventListener 'hashchange', rememberCurrentUrlAndState, false
window.addEventListener 'popstate', onHistoryChange, false

browserSupportsPushState = window.history and 'pushState' of window.history and 'state' of window.history

# Copied from github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js ua = navigator.userAgent browserIsBuggy =

(ua.indexOf('Android 2.') != -1 or ua.indexOf('Android 4.0') != -1) and
ua.indexOf('Mobile Safari') != -1 and
ua.indexOf('Chrome') == -1 and
ua.indexOf('Windows Phone') == -1

requestMethodIsSafe = popCookie('request_method') in ['GET','']

browserSupportsTurbolinks = browserSupportsPushState and !browserIsBuggy and requestMethodIsSafe

browserSupportsCustomEvents =

document.addEventListener and document.createEvent

if browserSupportsCustomEvents

installDocumentReadyPageEventTriggers()
installJqueryAjaxSuccessPageUpdateTrigger()

if browserSupportsTurbolinks

visit = fetch
initializeTurbolinks()

else

visit = (url = document.location.href) -> document.location.href = url

# Public API # Turbolinks.visit(url) # Turbolinks.replace(html) # Turbolinks.pagesCached() # Turbolinks.pagesCached(20) # Turbolinks.cacheCurrentPage() # Turbolinks.enableTransitionCache() # Turbolinks.disableRequestCaching() # Turbolinks.ProgressBar.enable() # Turbolinks.ProgressBar.disable() # Turbolinks.ProgressBar.start() # Turbolinks.ProgressBar.advanceTo(80) # Turbolinks.ProgressBar.done() # Turbolinks.allowLinkExtensions('md') # Turbolinks.supported # Turbolinks.EVENTS @Turbolinks = {

visit,
replace,
pagesCached,
cacheCurrentPage,
enableTransitionCache,
disableRequestCaching,
ProgressBar: ProgressBarAPI,
allowLinkExtensions: Link.allowExtensions,
supported: browserSupportsTurbolinks,
EVENTS: clone(EVENTS)

}