Skip to content
This repository has been archived by the owner on Jul 28, 2018. It is now read-only.

Commit

Permalink
Refactor click handling, link checking, and url parsing into classes;…
Browse files Browse the repository at this point in the history
… ensure absolute urls used for cache keys and state objects.
  • Loading branch information
reed committed Feb 9, 2014
1 parent edff68c commit 561c130
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 73 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,10 @@
## Turbolinks (master)

* Always use absolute URLs as cache keys and in state objects. Eliminates possibility of multiple
cache objects for the same page.

*Nick Reed*

## Turbolinks 2.2.1 (January 30, 2014) ## Turbolinks 2.2.1 (January 30, 2014)


* Do not store redirect_to location in session if the request did not come from Turbolinks. Fixes * Do not store redirect_to location in session if the request did not come from Turbolinks. Fixes
Expand Down
194 changes: 121 additions & 73 deletions lib/assets/javascripts/turbolinks.js.coffee
Expand Up @@ -4,7 +4,6 @@ transitionCacheEnabled = false


currentState = null currentState = null
loadedAssets = null loadedAssets = null
htmlExtensions = ['html']


referer = null referer = null


Expand All @@ -13,11 +12,13 @@ xhr = null




fetch = (url) -> fetch = (url) ->
url = new ComponentUrl url

rememberReferer() rememberReferer()
cacheCurrentPage() cacheCurrentPage()
reflectNewUrl url reflectNewUrl url


if transitionCacheEnabled and cachedPage = transitionCacheFor(url) if transitionCacheEnabled and cachedPage = transitionCacheFor(url.absolute)
fetchHistory cachedPage fetchHistory cachedPage
fetchReplacement url fetchReplacement url
else else
Expand All @@ -31,11 +32,11 @@ enableTransitionCache = (enable = true) ->
transitionCacheEnabled = enable transitionCacheEnabled = enable


fetchReplacement = (url, onLoadFunction = =>) -> fetchReplacement = (url, onLoadFunction = =>) ->
triggerEvent 'page:fetch', url: url triggerEvent 'page:fetch', url: url.absolute


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


Expand All @@ -48,10 +49,10 @@ fetchReplacement = (url, onLoadFunction = =>) ->
onLoadFunction() onLoadFunction()
triggerEvent 'page:load' triggerEvent 'page:load'
else else
document.location.href = url document.location.href = url.absolute


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


xhr.send() xhr.send()


Expand All @@ -63,8 +64,10 @@ fetchHistory = (cachedPage) ->




cacheCurrentPage = -> cacheCurrentPage = ->
pageCache[currentState.url] = currentStateUrl = new ComponentUrl currentState.url
url: document.location.href,
pageCache[currentStateUrl.absolute] =
url: currentStateUrl.relative,
body: document.body, body: document.body,
title: document.title, title: document.title,
positionY: window.pageYOffset, positionY: window.pageYOffset,
Expand Down Expand Up @@ -113,13 +116,14 @@ removeNoscriptTags = (node) ->
node node


reflectNewUrl = (url) -> reflectNewUrl = (url) ->
if url isnt referer if (url = new ComponentUrl url).absolute isnt referer
window.history.pushState { turbolinks: true, url: url }, '', url window.history.pushState { turbolinks: true, url: url.absolute }, '', url.absolute


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


rememberReferer = -> rememberReferer = ->
referer = document.location.href referer = document.location.href
Expand All @@ -140,17 +144,6 @@ resetScrollPosition = ->
window.scrollTo 0, 0 window.scrollTo 0, 0




# Intention revealing function alias
removeHashForIE10compatibility = (url) ->
removeHash url

removeHash = (url) ->
link = url
unless url.href?
link = document.createElement 'A'
link.href = url
link.href.replace link.hash, ''

popCookie = (name) -> popCookie = (name) ->
value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or '' value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/' document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
Expand Down Expand Up @@ -242,53 +235,108 @@ browserCompatibleDocumentParser = ->
return createDocumentUsingWrite return createDocumentUsingWrite




installClickHandlerLast = (event) -> # The ComponenetUrl class converts a basic URL string into an object
unless event.defaultPrevented # that behaves similarly to document.location.
document.removeEventListener 'click', handleClick, false #
document.addEventListener 'click', handleClick, false # If an instance is created from a relative URL, the current document

# is used to fill in the missing attributes (protocol, host, port).
handleClick = (event) -> class ComponentUrl
unless event.defaultPrevented constructor: (@original = document.location.href) ->
link = extractLink event return @original if @original.constructor.name is 'ComponentUrl'

This comment has been minimized.

Copy link
@twalpole

twalpole Feb 14, 2014

checking for the name of the constructor breaks when using uglifier to compress in the rails asset pipeline. This is because the class name of CompnentUrl is minified while the string is not changed. This means that when a component url is passed in from fetch -> reflectNewUrl it gets double converted into a ComponentUrl and breaks the url passed to window.history.pushState

if link.nodeName is 'A' and !ignoreClick(event, link) @_parse()
visit link.href unless pageChangePrevented()
event.preventDefault() withoutHash: -> @href.replace @hash, ''



# Intention revealing function alias
extractLink = (event) -> withoutHashForIE10compatibility: -> @withoutHash()
link = event.target
link = link.parentNode until !link.parentNode or link.nodeName is 'A' hasNoHash: -> @hash.length is 0
link

_parse: ->
crossOriginLink = (link) -> (@link ?= document.createElement 'a').href = @original
location.protocol isnt link.protocol or location.host isnt link.host { @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link

@origin = [@protocol, '//', @host].join ''
anchoredLink = (link) -> @relative = [@pathname, @search, @hash].join ''
((link.hash and removeHash(link)) is removeHash(location)) or @absolute = @href
(link.href is location.href + '#')

# The Link class derives from the ComponentUrl class, but is built from an
nonHtmlLink = (link) -> # existing link element. Provides verification functionality for Turbolinks
url = removeHash link # to use in determining whether it should process the link when clicked.
url.match(/\.[a-z]+(\?.*)?$/g) and not url.match(new RegExp("\\.(?:#{htmlExtensions.join('|')})?(\\?.*)?$", 'g')) class Link extends ComponentUrl

@HTML_EXTENSIONS: ['html']
noTurbolink = (link) ->
until ignore or link is document @allowExtensions: (extensions...) ->
ignore = link.getAttribute('data-no-turbolink')? Link.HTML_EXTENSIONS.push extension for extension in extensions
link = link.parentNode Link.HTML_EXTENSIONS
ignore

constructor: (@link) ->
targetLink = (link) -> return @link if @link.constructor.name is 'Link'

This comment has been minimized.

Copy link
@twalpole

twalpole Feb 14, 2014

Same issue as with ComponentUrl when used with uglifier

link.target.length isnt 0 @original = @link.href

super
nonStandardClick = (event) ->
event.which > 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.altKey shouldIgnore: ->

@_crossOrigin() or
ignoreClick = (event, link) -> @_anchored() or
crossOriginLink(link) or anchoredLink(link) or nonHtmlLink(link) or noTurbolink(link) or targetLink(link) or nonStandardClick(event) @_nonHtml() or

@_optOut() or
allowLinkExtensions = (extensions...) -> @_target()
htmlExtensions.push extension for extension in extensions
htmlExtensions _crossOrigin: ->
@origin isnt (new ComponentUrl).origin

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

_nonHtml: ->
url = @withoutHash()
url.match(/\.[a-z]+(\?.*)?$/g) and not url.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'

_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 # Delay execution of function long enough to miss the popstate event
Expand All @@ -310,7 +358,7 @@ installJqueryAjaxSuccessPageUpdateTrigger = ->


installHistoryChangeHandler = (event) -> installHistoryChangeHandler = (event) ->
if event.state?.turbolinks if event.state?.turbolinks
if cachedPage = pageCache[event.state.url] if cachedPage = pageCache[(new ComponentUrl(event.state.url)).absolute]
cacheCurrentPage() cacheCurrentPage()
fetchHistory cachedPage fetchHistory cachedPage
else else
Expand All @@ -321,7 +369,7 @@ initializeTurbolinks = ->
rememberCurrentState() rememberCurrentState()
createDocument = browserCompatibleDocumentParser() createDocument = browserCompatibleDocumentParser()


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


bypassOnLoadPopstate -> bypassOnLoadPopstate ->
window.addEventListener 'popstate', installHistoryChangeHandler, false window.addEventListener 'popstate', installHistoryChangeHandler, false
Expand Down Expand Up @@ -361,4 +409,4 @@ else
# Turbolinks.enableTransitionCache() # Turbolinks.enableTransitionCache()
# Turbolinks.allowLinkExtensions('md') # Turbolinks.allowLinkExtensions('md')
# Turbolinks.supported # Turbolinks.supported
@Turbolinks = { visit, pagesCached, enableTransitionCache, allowLinkExtensions, supported: browserSupportsTurbolinks } @Turbolinks = { visit, pagesCached, enableTransitionCache, allowLinkExtensions: Link.allowExtensions, supported: browserSupportsTurbolinks }

0 comments on commit 561c130

Please sign in to comment.