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 }