-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathHistory.js
245 lines (206 loc) · 6.73 KB
/
History.js
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
241
242
243
244
245
var qs = require('qs')
var parseUrl = require('url').parse
var resolveUrl = require('url').resolve
var router = require('./router')
var currentPath = window.location.pathname + window.location.search
// Replace the initial state with the current URL immediately,
// so that it will be rendered if the state is later popped
if (window.history.replaceState) {
window.history.replaceState({
$render: true,
$method: 'get'
}, null, window.location.href)
}
module.exports = History
function History(app, routes) {
this.app = app
this.routes = routes
if (window.history.pushState) {
addListeners(this)
return
}
this.push = function(url) {
window.location.assign(url)
}
this.replace = function(url) {
window.location.replace(url)
}
}
History.prototype.push = function(url, render, state, e) {
this._update('pushState', url, render, state, e)
}
History.prototype.replace = function(url, render, state, e) {
this._update('replaceState', url, render, state, e)
}
// Rerender the current url locally
History.prototype.refresh = function() {
var path = routePath(window.location.href)
// Note that we don't pass previous to avoid triggering transitions
router.render(this, {url: path, method: 'get'})
}
History.prototype.back = function() {
window.history.back()
}
History.prototype.forward = function() {
window.history.forward()
}
History.prototype.go = function(i) {
window.history.go(i)
}
History.prototype._update = function(historyMethod, relativeUrl, render, state, e) {
var url = resolveUrl(window.location.href, relativeUrl)
var path = routePath(url)
// TODO: history.push should set the window.location with external urls
if (!path) return
if (render == null) render = true
if (state == null) state = {}
// Update the URL
var options = renderOptions(e, path)
state.$render = true
state.$method = options.method
window.history[historyMethod](state, null, options.url + (url.hash || ''))
currentPath = window.location.pathname + window.location.search
if (render) router.render(this, options, e)
// Jump to named element on new page, to mimic browser behavior
if (url.hash) {
var hashElement = document.getElementById(url.hash.substring(1))
if (hashElement) {
hashElement.scrollIntoView()
}
}
}
History.prototype.page = function() {
var page = this.app.createPage()
var history = this
function redirect(url) {
if (url === 'back') return history.back()
// TODO: Add support for `basepath` option like Express
if (url === 'home') url = '\\'
history.replace(url, true)
}
page.redirect = redirect
return page
}
// Get the pathname if it is on the same protocol and domain
function routePath(url) {
var match = parseUrl(url)
return match &&
match.protocol === window.location.protocol &&
match.host === window.location.host &&
match.pathname + (match.search || '')
}
function renderOptions(e, path) {
// If this is a form submission, extract the form data and
// append it to the url for a get or params.body for a post
if (e && e.type === 'submit') {
var form = e.target
var elements = form.elements
var query = []
for (var i = 0, len = elements.length, el; i < len; i++) {
el = elements[i]
var name = el.name
if (!name) continue
var value = el.value
query.push(encodeURIComponent(name) + '=' + encodeURIComponent(value))
if (name === '_method') {
var override = value.toLowerCase()
if (override === 'delete') override = 'del'
}
}
query = query.join('&')
if (form.method.toLowerCase() === 'post') {
var method = override || 'post'
var body = qs.parse(query)
} else {
method = 'get'
path += '?' + query
}
} else {
method = 'get'
}
return {
method: method
, url: path
, previous: window.location.pathname + window.location.search
, body: body
, form: form
, link: e && e._tracksLink
}
}
function addListeners(history) {
// Detect clicks on links
function onClick(e) {
var el = e.target
// Ignore command click, control click, and non-left click
if (e.metaKey || e.which !== 1) return
// Ignore if already prevented
if (e.defaultPrevented) return
// Also look up for parent links (<a><img></a>)
while (el) {
var url = el.href
if (url) {
// Ignore if created by Tracks
if (el.hasAttribute && el.hasAttribute('data-router-ignore')) return
// Ignore links meant to open in a different window or frame
if (el.target && el.target !== '_self') return
// Ignore hash links to the same page
var hashIndex = url.indexOf('#')
if (~hashIndex && url.slice(0, hashIndex) === window.location.href.replace(/#.*/, '')) {
return
}
e._tracksLink = el
history.push(url, true, null, e)
return
}
el = el.parentNode
}
}
function onSubmit(e) {
var target = e.target
// Ignore if already prevented
if (e.defaultPrevented) return
// Only handle if emitted on a form element that isn't multipart
if (target.tagName.toLowerCase() !== 'form') return
if (target.enctype === 'multipart/form-data') return
// Ignore if created by Tracks
if (target.hasAttribute && target.hasAttribute('data-router-ignore')) return
// Use the url from the form action, defaulting to the current url
var url = target.action || window.location.href
history.push(url, true, null, e)
}
function onPopState(e) {
// HACK: Chrome sometimes does a pop state before the app is set up properly
if (!history.app.page) return
var previous = currentPath
var state = e.state
currentPath = window.location.pathname + window.location.search
var options = {
previous: previous
, url: currentPath
}
if (state) {
if (!state.$render) return
options.method = state.$method
// Note that the post body is only sent on the initial reqest
// and it is empty if the state is later popped
return router.render(history, options)
}
// The state object will be null for states created by jump links.
// window.location.hash cannot be used, because it returns nothing
// if the url ends in just a hash character
var url = window.location.href
, hashIndex = url.indexOf('#')
, el, id
if (~hashIndex && currentPath !== previous) {
options.method = 'get'
router.render(history, options)
id = url.slice(hashIndex + 1)
if (el = document.getElementById(id) || document.getElementsByName(id)[0]) {
el.scrollIntoView()
}
}
}
document.addEventListener('click', onClick, true)
document.addEventListener('submit', onSubmit, false)
window.addEventListener('popstate', onPopState, true)
}