TRACKED_ASSET_SELECTOR = '[data-turbolinks-track]' TRACKED_ATTRIBUTE_NAME = 'turbolinksTrack' ANONYMOUS_TRACK_VALUE = 'true'
scriptPromises = {} resolvePreviousRequest = null
waitForCompleteDownloads = ->
loadingPromises = Object.keys(scriptPromises).map (url) -> scriptPromises[url] Promise.all(loadingPromises)
class TurboGraft
.TurboHead
constructor: (@activeDocument, @upstreamDocument) -> @activeAssets = extractTrackedAssets(@activeDocument) @upstreamAssets = extractTrackedAssets(@upstreamDocument) @newScripts = @upstreamAssets .filter(attributeMatches('nodeName', 'SCRIPT')) .filter(noAttributeMatchesIn('src', @activeAssets)) @newLinks = @upstreamAssets .filter(attributeMatches('nodeName', 'LINK')) .filter(noAttributeMatchesIn('href', @activeAssets)) @_testAPI: { reset: -> scriptPromises = {} resolvePreviousRequest = null } hasChangedAnonymousAssets: () -> anonymousUpstreamAssets = @upstreamAssets .filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE)) anonymousActiveAssets = @activeAssets .filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE)) if anonymousActiveAssets.length != anonymousUpstreamAssets.length return true noMatchingSrc = noAttributeMatchesIn('src', anonymousUpstreamAssets) noMatchingHref = noAttributeMatchesIn('href', anonymousUpstreamAssets) anonymousActiveAssets.some((node) -> noMatchingSrc(node) || noMatchingHref(node) ) movingFromTrackedToUntracked: () -> @upstreamAssets.length == 0 && @activeAssets.length > 0 hasNamedAssetConflicts: () -> @newScripts .concat(@newLinks) .filter(noDatasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE)) .some(datasetMatchesIn(TRACKED_ATTRIBUTE_NAME, @activeAssets)) hasAssetConflicts: () -> @movingFromTrackedToUntracked() || @hasNamedAssetConflicts() || @hasChangedAnonymousAssets() waitForAssets: () -> resolvePreviousRequest?(isCanceled: true) new Promise((resolve) => resolvePreviousRequest = resolve waitForCompleteDownloads() .then(@_insertNewAssets) .then(waitForCompleteDownloads) .then(resolve) ) _insertNewAssets: () => updateLinkTags(@activeDocument, @newLinks) updateScriptTags(@activeDocument, @newScripts)
extractTrackedAssets = (doc) ->
[].slice.call(doc.querySelectorAll(TRACKED_ASSET_SELECTOR))
attributeMatches = (attribute, value) ->
(node) -> node[attribute] == value
attributeMatchesIn = (attribute, collection) ->
(node) -> collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
noAttributeMatchesIn = (attribute, collection) ->
(node) -> !collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
datasetMatches = (attribute, value) ->
(node) -> node.dataset[attribute] == value
noDatasetMatches = (attribute, value) ->
(node) -> node.dataset[attribute] != value
datasetMatchesIn = (attribute, collection) ->
(node) -> value = node.dataset[attribute] collection.some(datasetMatches(attribute, value))
noDatasetMatchesIn = (attribute, collection) ->
(node) -> value = node.dataset[attribute] !collection.some(datasetMatches(attribute, value))
updateLinkTags = (activeDocument, newLinks) ->
# style tag load events don't work in all browsers # as such we just hope they load ¯\_(ツ)_/¯ newLinks.forEach((linkNode) -> newNode = linkNode.cloneNode() activeDocument.head.appendChild(newNode) triggerEvent("page:after-link-inserted", newNode) )
updateScriptTags = (activeDocument, newScripts) ->
promise = Promise.resolve() newScripts.forEach (scriptNode) -> promise = promise.then(-> insertScript(activeDocument, scriptNode)) promise
insertScript = (activeDocument, scriptNode) ->
url = scriptNode.src if scriptPromises[url] return scriptPromises[url] # Clone script tags to guarantee browser execution. newNode = activeDocument.createElement('SCRIPT') newNode.setAttribute(attr.name, attr.value) for attr in scriptNode.attributes newNode.appendChild(activeDocument.createTextNode(scriptNode.innerHTML)) scriptPromises[url] = new Promise((resolve) -> onAssetEvent = (event) -> triggerEvent("page:#script-error", event) if event.type == 'error' newNode.removeEventListener('load', onAssetEvent) newNode.removeEventListener('error', onAssetEvent) resolve() newNode.addEventListener('load', onAssetEvent) newNode.addEventListener('error', onAssetEvent) activeDocument.head.appendChild(newNode) triggerEvent("page:after-script-inserted", newNode) )