cacheSize = 10 currentState = null referer = null loadedAssets = null pageCache = {} createDocument = null requestMethod = document.cookie.match(/request_method=(w+)/)?.toUpperCase() or '' xhr = null
fetchReplacement = (url) ->
triggerEvent 'page:fetch'
# Remove hash from url to ensure IE 10 compatibility
safeUrl = removeHash url
xhr?.abort()
xhr = new XMLHttpRequest
xhr.open 'GET', safeUrl, true
xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
xhr.setRequestHeader 'X-XHR-Referer', referer
xhr.onload = ->
triggerEvent 'page:receive'
if doc = processResponse()
reflectNewUrl url
changePage extractTitleAndBody(doc)...
reflectRedirectedUrl()
if document.location.hash
document.location.href = document.location.href
else
resetScrollPosition()
triggerEvent 'page:load'
else
document.location.href = url
xhr.onloadend = -> xhr = null
xhr.onabort = -> rememberCurrentUrl()
xhr.onerror = -> document.location.href = url
xhr.send()
fetchHistory = (position) ->
cacheCurrentPage() page = pageCache[position] xhr?.abort() changePage page.title, page.body recallScrollPosition page triggerEvent 'page:restore'
cacheCurrentPage = ->
pageCache[currentState.position] = url: document.location.href, body: document.body, title: document.title, positionY: window.pageYOffset, positionX: window.pageXOffset constrainPageCacheTo cacheSize
pagesCached = (size = cacheSize) ->
cacheSize = parseInt(size) if /^[\d]+$/.test size
constrainPageCacheTo = (limit) ->
for own key, value of pageCache pageCache[key] = null if key <= currentState.position - limit return
changePage = (title, body, csrfToken, runScripts) ->
document.title = title document.documentElement.replaceChild body, document.body CSRFToken.update csrfToken if csrfToken? removeNoscriptTags() executeScriptTags() if runScripts currentState = window.history.state triggerEvent 'page:change'
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 = ->
noscriptTags = Array::slice.call document.body.getElementsByTagName 'noscript' noscript.parentNode.removeChild noscript for noscript in noscriptTags return
reflectNewUrl = (url) ->
if url isnt referer
window.history.pushState { turbolinks: true, position: currentState.position + 1 }, '', url
reflectRedirectedUrl = ->
if location = xhr.getResponseHeader 'X-XHR-Redirected-To' preservedHash = if removeHash(location) is location then document.location.hash else '' window.history.replaceState currentState, '', location + preservedHash
rememberCurrentUrl = ->
window.history.replaceState { turbolinks: true, position: Date.now() }, '', document.location.href
rememberCurrentState = ->
currentState = window.history.state
recallScrollPosition = (page) ->
window.scrollTo page.positionX, page.positionY
resetScrollPosition = ->
window.scrollTo 0, 0
removeHash = (url) ->
link = url unless url.href? link = document.createElement 'A' link.href = url link.href.replace link.hash, ''
triggerEvent = (name) ->
event = document.createEvent 'Events' 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) ->
(node.src || node.href) for node in doc.head.childNodes when node.getAttribute?('data-turbolinks-track')?
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, 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
installClickHandlerLast = (event) ->
unless event.defaultPrevented document.removeEventListener 'click', handleClick, false document.addEventListener 'click', handleClick, false
handleClick = (event) ->
unless event.defaultPrevented
link = extractLink event
if link.nodeName is 'A' and !ignoreClick(event, link)
visit link.href unless pageChangePrevented()
event.preventDefault()
extractLink = (event) ->
link = event.target link = link.parentNode until !link.parentNode or link.nodeName is 'A' link
crossOriginLink = (link) ->
location.protocol isnt link.protocol or location.host isnt link.host
anchoredLink = (link) ->
((link.hash and removeHash(link)) is removeHash(location)) or (link.href is location.href + '#')
nonHtmlLink = (link) ->
url = removeHash link url.match(/\.[a-z]+(\?.*)?$/g) and not url.match(/\.html?(\?.*)?$/g)
noTurbolink = (link) ->
until ignore or link is document
ignore = link.getAttribute('data-no-turbolink')?
link = link.parentNode
ignore
targetLink = (link) ->
link.target.length isnt 0
nonStandardClick = (event) ->
event.which > 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.altKey
ignoreClick = (event, link) ->
crossOriginLink(link) or anchoredLink(link) or nonHtmlLink(link) or noTurbolink(link) or targetLink(link) or nonStandardClick(event)
initializeTurbolinks = ->
rememberCurrentUrl()
rememberCurrentState()
createDocument = browserCompatibleDocumentParser()
document.addEventListener 'click', installClickHandlerLast, true
window.addEventListener 'popstate', (event) ->
state = event.state
if state?.turbolinks
if pageCache[state.position]
fetchHistory state.position
else
visit event.target.location.href
, false
browserSupportsPushState =
window.history and window.history.pushState and window.history.replaceState and window.history.state != undefined
browserIsntBuggy =
!navigator.userAgent.match /CriOS\//
requestMethodIsSafe =
requestMethod in ['GET','']
if browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe
visit = (url) -> referer = document.location.href cacheCurrentPage() fetchReplacement url initializeTurbolinks()
else
visit = (url) -> document.location.href = url
# Public API # Turbolinks.visit(url) # Turbolinks.pagesCached() # Turbolinks.pagesCached(20) @Turbolinks = { visit, pagesCached }