Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
496 lines (396 sloc) 16.303 kb
pageCache = {}
cacheSize = 10
transitionCacheEnabled = false
currentState = null
loadedAssets = null
referer = null
createDocument = null
xhr = null
EVENTS =
BEFORE_CHANGE: 'page:before-change'
FETCH: 'page:fetch'
RECEIVE: 'page:receive'
CHANGE: 'page:change'
UPDATE: 'page:update'
LOAD: 'page:load'
RESTORE: 'page:restore'
BEFORE_UNLOAD: 'page:before-unload'
EXPIRE: 'page:expire'
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 EVENTS.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 EVENTS.RECEIVE, url: url.absolute
if doc = processResponse()
reflectNewUrl url
changePage extractTitleAndBody(doc)...
manuallyTriggerHashChangeForFirefox()
reflectRedirectedUrl()
onLoadFunction()
triggerEvent EVENTS.LOAD
else
document.location.href = crossOriginRedirect() or 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 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]')?
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 EVENTS.EXPIRE, pageCache[key]
delete pageCache[key]
changePage = (title, body, csrfToken, runScripts) ->
triggerEvent EVENTS.BEFORE_UNLOAD
document.title = title
document.documentElement.replaceChild body, document.body
CSRFToken.update csrfToken if csrfToken?
setAutofocusElement()
executeScriptTags() if runScripts
currentState = window.history.state
triggerEvent EVENTS.CHANGE
triggerEvent EVENTS.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.async = false unless script.hasAttribute 'async'
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
crossOriginRedirect = ->
redirect if (redirect = xhr.getResponseHeader('Location'))? and (new ComponentUrl(redirect)).crossOrigin()
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
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
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) ->
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)(?:;|$)/
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()
doc = createDocument xhr.responseText
if doc and !assetsChanged doc
return doc
extractTitleAndBody = (doc) ->
title = doc.querySelector 'title'
[ title?.textContent, removeNoscriptTags(doc.querySelector('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
createDocumentUsingFragment = (html) ->
head = html.match(/<head[^>]*>([\s\S.]*)<\/head>/i)?[0] or '<head></head>'
body = html.match(/<body[^>]*>([\s\S.]*)<\/body>/i)?[0] or '<body></body>'
htmlWrapper = document.createElement 'html'
htmlWrapper.innerHTML = head + body
doc = document.createDocumentFragment()
doc.appendChild htmlWrapper
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)
#
# Use createDocumentUsingFragment if the previously selected parser does not
# correctly parse <form> tags. (Safari 7.1+ - see github.com/rails/turbolinks/issues/408)
buildTestsUsing = (createMethod) ->
buildTest = (fallback, passes) ->
passes: passes()
fallback: fallback
structureTest = buildTest createDocumentUsingWrite, =>
(createMethod '<html><body><p>test')?.body?.childNodes.length is 1
formNestingTest = buildTest createDocumentUsingFragment, =>
(createMethod '<html><body><form></form><div></div></body></html>')?.body?.childNodes.length is 2
[structureTest, formNestingTest]
try
if window.DOMParser
docTests = buildTestsUsing createDocumentUsingParser
createDocumentUsingParser
catch e
docTests = buildTestsUsing createDocumentUsingDOM
createDocumentUsingDOM
finally
for docTest in docTests
return docTest.fallback unless docTest.passes
# 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
_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 unless pageChangePrevented(@link.absolute)
@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 EVENTS.CHANGE
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
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.EVENTS
@Turbolinks = {
visit,
pagesCached,
enableTransitionCache,
allowLinkExtensions: Link.allowExtensions,
supported: browserSupportsTurbolinks,
EVENTS: clone(EVENTS)
}
Jump to Line
Something went wrong with that request. Please try again.