forked from assaf/zombie
-
Notifications
You must be signed in to change notification settings - Fork 0
/
history.coffee
240 lines (220 loc) · 8.93 KB
/
history.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# Window history and location.
http = require("http")
jsdom = require("jsdom")
html = jsdom.dom.level3.html
qs = require("querystring")
URL = require("url")
util = require("util")
# 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
this.update = (url)->
@url = URL.parse(URL.format(url))
@location = new Location(history, @url)
this.update url
# ## window.history
#
# Represents window.history.
class History
constructor: (browser)->
# History is a stack of Entry objects.
stack = []
index = -1
# 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 = browser.window.document.createEvent("HTMLEvents")
evt.initEvent "hashchange", true, false
browser.window.dispatchEvent 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()
throw new Error("Cannot load resource: #{URL.format(url)}") unless url.protocol && url.hostname
# 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 browser.window.document
browser.open history: this, interactive: browser.window.parent == browser.window
# 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 =
url: URL.format(url)
deferClose: false
parser: require("html5").HTML5
features:
QuerySelector: true
ProcessExternalResources: []
FetchExternalResources: []
if browser.runScripts
options.features.ProcessExternalResources.push "script"
options.features.FetchExternalResources.push "script"
options.features.FetchExternalResources.push "iframe"
document = jsdom.jsdom(false, jsdom.level3, options)
document.fixQueue()
browser.window.document = document
headers = if headers then JSON.parse(JSON.stringify(headers)) else {}
referer = stack[index-1]?.url
headers["referer"] = referer.href if referer?
browser.window.resources.request method, url, data, headers, (error, response)=>
if error
event = document.createEvent("HTMLEvents")
event.initEvent "error", true, false
document.dispatchEvent event
browser.emit "error", error
else
browser.response = [response.statusCode, response.headers, response.body]
stack[index].update response.url
body = if response.body.trim() == "" then "<html></html>" else response.body
document.open()
document.write body
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
new_index = stack.length - 1 if stack.length > 0 && new_index >= stack.length
if new_index != index && entry = stack[new_index]
index = new_index
if entry.pop
if browser.window.document
# Created with pushState/replaceState, send popstate event
evt = browser.window.document.createEvent("HTMLEvents")
evt.initEvent "popstate", false, false
evt.state = entry.state
browser.window.dispatchEvent evt
# Do not load different page unless we're on a different host
resource stack[index] if was.host != stack[index].host
else
pageChanged was
return
# ### history.length => Number
#
# Number of states/URLs in the history.
@__defineGetter__ "length", -> stack.length
# ### history.pushState(state, title, url)
#
# Push new state to the stack, do not reload
@pushState = (state, title, 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
stack[index] = new Entry(this, url, { state: state, title: title, pop: true })
# Location uses this to move to a new URL.
@_assign = (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)->
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.
#
# * url -- Same as form action, can be relative to current document
# * method -- Method to use, defaults to GET
# * data -- Form valuesa
# * enctype -- Encoding type, or use default
@_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
# Add Location/History to window.
this.extend = (new_window)->
new_window.__defineGetter__ "history", => this
new_window.__defineGetter__ "location", => stack[index]?.location || new Location(this, {})
new_window.__defineSetter__ "location", (url)=> @_assign URL.resolve(stack[index]?.url, url)
# Used to dump state to console (debuggin)
this.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
this.save = ->
serialized = []
for i, entry of stack
line = URL.format(entry.url)
line += " #{JSON.stringify(entry.state)}" if entry.pop
serialized.push line
serialized.join("\n")
# browser.loadHistory uses this
this.load = (serialized) ->
for line in serialized.split(/\n+/)
[url, state] = line.split(/\s/)
options = state && { state: JSON.parse(state), title: null, pop: true }
stack[++index] = new Entry(this, url, state)
# ## window.location
#
# Represents 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 = -> URL.format(url)
# ### location.href => String
@__defineGetter__ "href", -> url?.href
# ### location.href = url
@__defineSetter__ "href", (url)-> history._assign url
# Getter/setter for location parts.
for prop in ["hash", "host", "hostname", "pathname", "port", "protocol", "search"]
do (prop)=>
@__defineGetter__ prop, -> url?[prop] || ""
@__defineSetter__ prop, (value)->
newUrl = URL.parse(url?.href)
newUrl[prop] = value
history._assign URL.format(newUrl)
# ## document.location => Location
#
# document.location is same as window.location
html.HTMLDocument.prototype.__defineGetter__ "location", -> @parentWindow.location
exports.History = History