history.coffee | |
---|---|
Window history and location. | util = require("util")
JSDOM = require("jsdom")
HTML = JSDOM.dom.level3.html
URL = require("url") |
History entry. Consists of: - state -- As provided by pushState/replaceState - title -- As provided by pushState/replaceState - pop -- True if added using pushState/replaceState - url -- URL object of current location - location -- Location object | class Entry
constructor: (@history, url, options)->
if options
@state = options.state
@title = options.title
@pop = !!options.pop
@update url
update: (url)->
@url = URL.parse(URL.format(url))
@location = new Location(@history, @url) |
window.historyRepresents window.history. | class History
constructor: (window)->
@_apply window |
History is a stack of Entry objects. | @_stack = []
@_index = -1 |
Apply to window. | _apply: (@_window)->
@_browser = @_window.browser |
Add Location/History to window. | Object.defineProperty @_window, "location",
get: =>
return @_stack[@_index]?.location || new Location(this, {})
set: (url)=>
@_assign URL.resolve(@_stack[@_index]?.url, url) |
Called when we switch to a new page with the URL of the old page. | _pageChanged: (was)->
url = @_stack[@_index]?.url
if !was || was.host != url.host || was.pathname != url.pathname || was.query != url.query |
We're on a different site or different page, load it | @_resource url
else if was.hash != url.hash |
Hash changed. Do not reload page, but do send hashchange | evt = @_window.document.createEvent("HTMLEvents")
evt.initEvent "hashchange", true, false
@_browser.dispatchEvent @_window, evt
else |
Load new page for now (but later on use caching). | @_resource url |
Make a request to external resource. We use this to fetch pages and submit forms, see _loadPage and _submit. | _resource: (url, method, data, headers)->
method = (method || "GET").toUpperCase()
unless url.protocol == "file:" || (url.protocol && url.hostname)
throw new Error("Cannot load resource: #{URL.format(url)}") |
If the browser has a new window, use it. If a document was already loaded into that window it would have state information we don't want (e.g. window.$) so open a new window. | if @_window.top == @_window.parent && @_window.document
newWindow = @_browser.open(history: this)
@_apply newWindow |
Create new DOM Level 3 document, add features (load external resources, etc) and associate it with current document. From this point on the browser sees a new document, client register event handler for DOMContentLoaded/error. | options =
deferClose: false
features:
QuerySelector: true
MutationEvents: "2.0"
ProcessExternalResources: []
FetchExternalResources: ["frame"]
parser: @_browser.htmlParser
url: URL.format(url)
if @_browser.runScripts
options.features.ProcessExternalResources.push "script"
options.features.FetchExternalResources.push "script"
if @_browser.loadCSS
options.features.FetchExternalResources.push "css"
document = JSDOM.jsdom(null, HTML, options)
@_window.document = document
document.window = document.parentWindow = @_window
headers = if headers then JSON.parse(JSON.stringify(headers)) else {}
referer = @_stack[@_index-1]?.url
headers["referer"] = referer.href if referer?
if credentials = @_browser.credentials
switch credentials.scheme.toLowerCase()
when "basic"
base64 = new Buffer(credentials.user + ":" + credentials.password).toString("base64")
headers["authorization"] = "Basic #{base64}"
when "bearer"
headers["authorization"] = "Bearer #{credentials.token}"
when "oauth"
headers["authorization"] = "OAuth #{credentials.token}"
@_browser.resources.request method, url, data, headers, (error, response)=>
if error
document.write "<html><body>#{error}</body></html>"
document.close()
@_browser.emit "error", error
else
@_browser.response = [response.statusCode, response.headers, response.body]
@_stack[@_index].update response.url
html = if response.body.trim() == "" then "<html><body></body></html>" else response.body
document.write html
document.close()
if document.documentElement
@_browser.emit "loaded", @_browser
else
error = "Could not parse document at #{URL.format(url)}" |
history.forward() | forward: -> @go(1) |
history.back() | back: -> @go(-1) |
history.go(amount) | go: (amount)->
was = @_stack[@_index]?.url
new_index = @_index + amount
new_index = 0 if new_index < 0
if @_stack.length > 0 && new_index >= @_stack.length
new_index = @_stack.length - 1
if new_index != @_index && entry = @_stack[new_index]
@_index = new_index
if entry.pop
if @_window.document |
Created with pushState/replaceState, send popstate event | evt = @_window.document.createEvent("HTMLEvents")
evt.initEvent "popstate", false, false
evt.state = entry.state
@_browser.dispatchEvent @_window, evt |
Do not load different page unless we're on a different host | if was.host != @_stack[@_index].host
@_resource @_stack[@_index].url
else
@_pageChanged was
return
|
history.length => NumberNumber of states/URLs in the history. | @prototype.__defineGetter__ "length", ->
return @_stack.length |
history.pushState(state, title, url)Push new state to the stack, do not reload | pushState: (state, title, url)->
url = URL.resolve(@_stack[@_index]?.url, url)
@_stack[++@_index] = new Entry(this, url, { state: state, title: title, pop: true }) |
history.replaceState(state, title, url)Replace existing state in the stack, do not reload | replaceState: (state, title, url)->
@_index = 0 if @_index < 0
url = URL.resolve(@_stack[@_index]?.url, url)
@_stack[@_index] = new Entry(this, url, { state: state, title: title, pop: true }) |
Location uses this to move to a new URL. | _assign: (url)->
url = URL.resolve(@_stack[@_index]?.url, url)
was = @_stack[@_index]?.url # before we destroy stack
@_stack = @_stack[0..@_index]
@_stack[++@_index] = new Entry(this, url)
@_pageChanged was |
Location uses this to load new page without changing history. | _replace: (url)->
url = URL.resolve(@_stack[@_index]?.url, url)
was = @_stack[@_index]?.url # before we destroy stack
@_index = 0 if @_index < 0
@_stack[@_index] = new Entry(this, url)
@_pageChanged was |
Location uses this to force a reload (location.reload), history uses this whenever we switch to a different page and need to load it. | _loadPage: (force)->
@_resource @_stack[@_index].url if @_stack[@_index]
|
Form submission. Makes request and loads response in the background.
| _submit: (url, method, data, enctype)->
headers = { "content-type": enctype || "application/x-www-form-urlencoded" }
@_stack = @_stack[0..@_index]
url = URL.resolve(@_stack[@_index]?.url, url)
@_stack[++@_index] = new Entry(this, url)
@_resource @_stack[@_index].url, method, data, headers |
Used to dump state to console (debuggin) | dump: ->
dump = []
for i, entry of @_stack
i = Number(i)
line = if i == @_index then "#{i + 1}: " else "#{i + 1}. "
line += URL.format(entry.url)
line += " state: " + util.inspect(entry.state) if entry.state
dump.push line
dump |
browser.saveHistory uses this | save: ->
serialized = []
for i, entry of @_stack
line = URL.format(entry.url)
line += " #{JSON.stringify(entry.state)}" if entry.pop
serialized.push line
return serialized.join("\n") + "\n" |
browser.loadHistory uses this | load: (serialized) ->
for line in serialized.split(/\n+/)
line = line.trim()
continue if line[0] == "#" || line == ""
[url, state] = line.split(/\s/)
options = state && { state: JSON.parse(state), title: null, pop: true }
@_stack[++@_index] = new Entry(this, url, state) |
window.locationRepresents window.location and document.location. | class Location
constructor: (@_history, @_url)-> |
location.assign(url) | assign: (newUrl)->
@_history._assign newUrl |
location.replace(url) | replace: (newUrl)->
@_history._replace newUrl |
location.reload(force?) | reload: (force)->
@_history._loadPage(force) |
location.toString() => String | toString: ->
return URL.format(@_url) |
location.href => String | @prototype.__defineGetter__ "href", ->
return @_url?.href |
location.href = url | @prototype.__defineSetter__ "href", (new_url)->
@_history._assign URL.resolve(@_url, new_url) |
Getter/setter for location parts. | for prop in ["hash", "host", "hostname", "pathname", "port", "protocol", "search"]
do (prop)=>
@prototype.__defineGetter__ prop, ->
@_url?[prop] || ""
@prototype.__defineSetter__ prop, (value)->
newUrl = URL.parse(@_url?.href)
newUrl[prop] = value
@_history._assign URL.format(newUrl) |
document.location => Locationdocument.location is same as window.location | HTML.HTMLDocument.prototype.__defineGetter__ "location", ->
@parentWindow.location
HTML.HTMLDocument.prototype.__defineSetter__ "location", (url)->
@parentWindow.location = url
exports.History = History
|