turbolinks.js.coffee - turbolinks-2.3.0 Documentation

pageCache = {} cacheSize = 10 transitionCacheEnabled = false

currentState = null loadedAssets = null

referer = null

createDocument = null xhr = null

fetch = (url) ->

    url = new ComponentUrl url

    rememberReferer()
    cacheCurrentPage()

    if transitionCacheEnabled and cachedPage = transitionCacheFor(url.absolute)
      fetchHistory cachedPage
      fetchReplacement url
    else
      fetchReplacement url, resetScrollPosition

transitionCacheFor = (url) ->

    cachedPage = pageCache[url]
    cachedPage if cachedPage and !cachedPage.transitionCacheDisabled

enableTransitionCache = (enable = true) ->

    transitionCacheEnabled = enable

fetchReplacement = (url, onLoadFunction = =>) ->

    triggerEvent 'page:fetch', url: url.absolute

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

    xhr.onload = ->
      triggerEvent 'page:receive', url: url.absolute

      if doc = processResponse()
        reflectNewUrl url
        changePage extractTitleAndBody(doc)...
        manuallyTriggerHashChangeForFirefox()
        reflectRedirectedUrl()
        onLoadFunction()
        triggerEvent 'page:load'
      else
        document.location.href = url.absolute

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

    xhr.send()

fetchHistory = (cachedPage) ->

    xhr?.abort()
    changePage cachedPage.title, cachedPage.body
    recallScrollPosition cachedPage
    triggerEvent 'page: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]')?

    constrainPageCacheTo cacheSize

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]
      triggerEvent 'page:expire', pageCache[key]
      delete pageCache[key]

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

    document.title = title
    document.documentElement.replaceChild body, document.body
    CSRFToken.update csrfToken if csrfToken?
    setAutofocusElement()
    executeScriptTags() if runScripts
    currentState = window.history.state
    triggerEvent 'page:change'
    triggerEvent 'page:update'

executeScriptTags = ->

    scripts = Array::slice.call document.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
    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.appendChild document.createTextNode script.innerHTML
      { parentNode, nextSibling } = script
      parentNode.removeChild script
      parentNode.insertBefore copy, nextSibling
    return

removeNoscriptTags = (node) ->

    node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
    node

# 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 isnt referer
      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 currentState, '', location.href + preservedHash

rememberReferer = ->

    referer = document.location.href

rememberCurrentUrl = ->

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

rememberCurrentState = ->

    currentState = window.history.state

# Unlike other browsers, Firefox doesn't trigger hashchange after changing the # location (via pushState) to an anchor on a different page. For example: # # /pages/one => /pages/two#with-hash # # By forcing Firefox to trigger hashchange, the rest of the code can rely on more # consistent behavior across browsers. manuallyTriggerHashChangeForFirefox = ->

    if navigator.userAgent.match(/Firefox/) and !(url = (new ComponentUrl)).hasNoHash()
      window.history.replaceState currentState, '', url.withoutHash()
      document.location.hash = url.hash

recallScrollPosition = (page) ->

    window.scrollTo page.positionX, page.positionY

resetScrollPosition = ->

    if document.location.hash
      document.location.href = document.location.href
    else
      window.scrollTo 0, 0

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

triggerEvent = (name, data) ->

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

pageChangePrevented = ->

    !triggerEvent 'page:before-change'

processResponse = ->

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

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

    extractTrackAssets = (doc) ->
      for node in doc.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()
      doc = createDocument xhr.responseText
      if doc and !assetsChanged doc
        return doc

extractTitleAndBody = (doc) ->

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

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

browserCompatibleDocumentParser = ->

    createDocumentUsingParser = (html) ->
      (new DOMParser).parseFromString html, 'text/html'

    createDocumentUsingDOM = (html) ->
      doc = document.implementation.createHTMLDocument ''
      doc.documentElement.innerHTML = html
      doc

    createDocumentUsingWrite = (html) ->
      doc = document.implementation.createHTMLDocument ''
      doc.open 'replace'
      doc.write html
      doc.close()
      doc

    # Use createDocumentUsingParser if DOMParser is defined and natively
    # supports 'text/html' parsing (Firefox 12+, IE 10)
    #
    # Use createDocumentUsingDOM if createDocumentUsingParser throws an exception
    # due to unsupported type 'text/html' (Firefox < 12, Opera)
    #
    # Use createDocumentUsingWrite if:
    #  - DOMParser isn't defined
    #  - createDocumentUsingParser returns null due to unsupported type 'text/html' (Chrome, Safari)
    #  - createDocumentUsingDOM doesn't create a valid HTML document (safeguarding against potential edge cases)
    try
      if window.DOMParser
        testDoc = createDocumentUsingParser '<html><body><p>test'
        createDocumentUsingParser
    catch e
      testDoc = createDocumentUsingDOM '<html><body><p>test'
      createDocumentUsingDOM
    finally
      unless testDoc?.body?.childNodes.length is 1
        return createDocumentUsingWrite

# 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, ''

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

    hasNoHash: -> @hash.length is 0

    _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
      super

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

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

    _anchored: ->
      ((@hash and @withoutHash()) is (current = new ComponentUrl).withoutHash()) or
        (@href is current.href + '#')

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

    _optOut: ->
      link = @link
      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 unless pageChangePrevented()
        @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

# Delay execution of function long enough to miss the popstate event # some browsers fire on the initial page load. bypassOnLoadPopstate = (fn) ->

    setTimeout fn, 500

installDocumentReadyPageEventTriggers = ->

    document.addEventListener 'DOMContentLoaded', ( ->
      triggerEvent 'page:change'
      triggerEvent 'page:update'
    ), true

installJqueryAjaxSuccessPageUpdateTrigger = ->

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

installHistoryChangeHandler = (event) ->

    if event.state?.turbolinks
      if cachedPage = pageCache[(new ComponentUrl(event.state.url)).absolute]
        cacheCurrentPage()
        fetchHistory cachedPage
      else
        visit event.target.location.href

initializeTurbolinks = ->

    rememberCurrentUrl()
    rememberCurrentState()
    createDocument = browserCompatibleDocumentParser()

    document.addEventListener 'click', Click.installHandlerLast, true

    window.addEventListener 'hashchange', (event) ->
      rememberCurrentUrl()
      rememberCurrentState()
    , false
    bypassOnLoadPopstate ->
      window.addEventListener 'popstate', installHistoryChangeHandler, false

# Handle bug in Firefox 26/27 where history.state is initially undefined historyStateIsDefined =

    window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/

browserSupportsPushState =

    window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined

browserIsntBuggy =

    !navigator.userAgent.match /CriOS\//

requestMethodIsSafe =

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

browserSupportsTurbolinks = browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe

browserSupportsCustomEvents =

    document.addEventListener and document.createEvent

if browserSupportsCustomEvents

    installDocumentReadyPageEventTriggers()
    installJqueryAjaxSuccessPageUpdateTrigger()

if browserSupportsTurbolinks

    visit = fetch
    initializeTurbolinks()

else

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

# Public API # Turbolinks.visit(url) # Turbolinks.pagesCached() # Turbolinks.pagesCached(20) # Turbolinks.enableTransitionCache() # Turbolinks.allowLinkExtensions('md') # Turbolinks.supported @Turbolinks = { visit, pagesCached, enableTransitionCache, allowLinkExtensions: Link.allowExtensions, supported: browserSupportsTurbolinks }