Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/externals/javelin/lib/Quicksand.js
12242 views
1
/**
2
* @requires javelin-install
3
* @provides javelin-quicksand
4
* @javelin
5
*/
6
7
/**
8
* Sink into a hopeless, cold mire of limitless depth from which there is
9
* no escape.
10
*
11
* Captures navigation events (like clicking links and using the back button)
12
* and expresses them in Javascript instead, emulating complex native browser
13
* behaviors in a language and context ill-suited to the task.
14
*
15
* By doing this, you abandon all hope and retreat to a world devoid of light
16
* or goodness. However, it allows you to have persistent UI elements which are
17
* not disrupted by navigation. A tempting trade, surely?
18
*
19
* To cast your soul into the darkness, use:
20
*
21
* JX.Quicksand
22
* .setFrame(node)
23
* .start();
24
*/
25
JX.install('Quicksand', {
26
27
statics: {
28
_id: null,
29
_onpage: 0,
30
_cursor: 0,
31
_current: 0,
32
_content: {},
33
_responses: {},
34
_history: [],
35
_started: false,
36
_frameNode: null,
37
_contentNode: null,
38
_uriPatternBlacklist: [],
39
40
/**
41
* Start Quicksand, accepting a fate of eternal torment.
42
*/
43
start: function(first_response) {
44
var self = JX.Quicksand;
45
if (self._started) {
46
return;
47
}
48
49
JX.Stratcom.listen('click', 'tag:a', self._onclick);
50
JX.Stratcom.listen('history:change', null, self._onchange);
51
52
self._started = true;
53
var path = JX.$U(window.location).getRelativeURI();
54
self._id = window.history.state || 0;
55
var id = self._id;
56
self._onpage = id;
57
self._history.push({path: path, id: id});
58
59
self._responses[id] = first_response;
60
},
61
62
63
/**
64
* Set the frame node which Quicksand controls content for.
65
*/
66
setFrame: function(frame) {
67
var self = JX.Quicksand;
68
self._frameNode = frame;
69
return self;
70
},
71
72
73
getCurrentPageID: function() {
74
var self = JX.Quicksand;
75
if (self._id === null) {
76
self._id = window.history.state || 0;
77
}
78
return self._id;
79
},
80
81
/**
82
* Respond to the user clicking a link.
83
*
84
* After a long list of checks, we may capture and simulate the resulting
85
* navigation.
86
*/
87
_onclick: function(e) {
88
var self = JX.Quicksand;
89
90
if (!self._frameNode) {
91
// If Quicksand has no frame, bail.
92
return;
93
}
94
95
if (JX.Stratcom.pass()) {
96
// If something else handled the event, bail.
97
return;
98
}
99
100
if (!e.isNormalClick()) {
101
// If this is a right-click, control click, etc., bail.
102
return;
103
}
104
105
if (e.getNode('workflow')) {
106
// Because JX.Workflow also passes these events, it might still want
107
// the event. Don't trigger if there's a workflow node in the stack.
108
return;
109
}
110
111
var a = e.getNode('tag:a');
112
var href = a.href;
113
if (!href || !href.length) {
114
// If the <a /> the user clicked has no href, or the href is empty,
115
// bail.
116
return;
117
}
118
119
if (href[0] == '#') {
120
// If this is an anchor on the current page, bail.
121
return;
122
}
123
124
var uri = new JX.$U(href);
125
var here = new JX.$U(window.location);
126
if (uri.getDomain() != here.getDomain()) {
127
// If the link is off-domain, bail.
128
return;
129
}
130
131
if (uri.getFragment() && uri.getPath() == here.getPath()) {
132
// If the link has an anchor but points at the current path, bail.
133
// This is presumably a long-form anchor on the current page.
134
135
// TODO: This technically gets links which change query parameters
136
// wrong: they are navigation events but we won't Quicksand them.
137
return;
138
}
139
140
if (self._isURIOnBlacklist(uri)) {
141
// This URI is blacklisted as not navigable via Quicksand.
142
return;
143
}
144
145
// The fate of this action is sealed. Suck it into the depths.
146
e.kill();
147
148
// If we're somewhere in history (that is, the user has pressed the
149
// back button one or more times, putting us in a state where pressing
150
// the forward button would do something) and we're navigating forward,
151
// all the stuff ahead of us is about to become unreachable when we
152
// navigate. Throw it away.
153
var discard = (self._history.length - self._cursor) - 1;
154
for (var ii = 0; ii < discard; ii++) {
155
var obsolete = self._history.pop();
156
self._responses[obsolete.id] = false;
157
}
158
159
// Set up the new state and fire a request to fetch the page data.
160
var path = JX.$U(uri).getRelativeURI();
161
var id = ++self._id;
162
163
self._history.push({path: path, id: id});
164
JX.History.push(path, id);
165
166
self._cursor = (self._history.length - 1);
167
self._responses[id] = null;
168
self._current = id;
169
170
new JX.Workflow(href, {__quicksand__: true})
171
.setHandler(JX.bind(null, self._onresponse, id))
172
.start();
173
},
174
175
176
/**
177
* Receive a response from the server with page data e.g. content.
178
*
179
* Usually we'll dump it into the page, but if the user clicked very fast
180
* it might already be out of date.
181
*/
182
_onresponse: function(id, r) {
183
var self = JX.Quicksand;
184
185
// Before possibly updating the document, check if this response is still
186
// relevant.
187
188
// We don't save the new response if the user has already destroyed
189
// the navigation. They can do this by pressing back, then clicking
190
// another link before the response can load.
191
if (self._responses[id] === false) {
192
return;
193
}
194
195
// Otherwise, this data is still relevant (either data on the current
196
// page, or data for a page that's still somewhere in history), so we
197
// save it.
198
var new_content = JX.$H(r.content).getFragment();
199
self._content[id] = new_content;
200
self._responses[id] = r;
201
202
// If it's the current page, draw it into the browser. It might not be
203
// the current page if the user already clicked another link.
204
if (self._current == id) {
205
self._draw(true);
206
}
207
},
208
209
210
/**
211
* Draw the current page.
212
*
213
* After a navigation event or the arrival of page content, we paint it
214
* onto the page.
215
*/
216
_draw: function(from_server) {
217
var self = JX.Quicksand;
218
219
if (self._onpage == self._current) {
220
// Don't bother redrawing if we're already on the current page.
221
return;
222
}
223
224
if (!self._responses[self._current]) {
225
// If we don't have this page yet, we can't draw it. We'll draw it
226
// when it arrives.
227
return;
228
}
229
230
// Otherwise, we're going to replace the page content. First, save the
231
// current page content. Modern computers have lots and lots of RAM, so
232
// there is no way this could ever create a problem.
233
var old = window.document.createDocumentFragment();
234
while (self._frameNode.firstChild) {
235
JX.DOM.appendContent(old, self._frameNode.firstChild);
236
}
237
self._content[self._onpage] = old;
238
239
// Now, replace it with the new content.
240
JX.DOM.setContent(self._frameNode, self._content[self._current]);
241
// Let other things redraw, etc as necessary
242
JX.Stratcom.invoke(
243
'quicksand-redraw',
244
null,
245
{
246
newResponse: self._responses[self._current],
247
newResponseID: self._current,
248
oldResponse: self._responses[self._onpage],
249
oldResponseID: self._onpage,
250
fromServer: from_server
251
});
252
self._onpage = self._current;
253
254
// Scroll to the top of the page and trigger any layout adjustments.
255
// TODO: Maybe store the scroll position?
256
JX.DOM.scrollToPosition(0, 0);
257
JX.Stratcom.invoke('resize');
258
},
259
260
261
/**
262
* Handle navigation events.
263
*
264
* In general, we're going to pull the content out of our history and dump
265
* it into the document.
266
*/
267
_onchange: function(e) {
268
var self = JX.Quicksand;
269
270
var data = e.getData();
271
data.state = data.state || null;
272
273
// Check if we're going back to the first page we started Quicksand on.
274
// We don't have a state value, but can look at the path.
275
if (data.state === null) {
276
if (JX.$U(window.location).getPath() == self._history[0].path) {
277
data.state = 0;
278
}
279
}
280
281
// Figure out where in history the user jumped to.
282
if (data.state !== null) {
283
self._current = data.state;
284
285
// Point the cursor at the right place in history.
286
for (var ii = 0; ii < self._history.length; ii++) {
287
if (self._history[ii].id == self._current) {
288
self._cursor = ii;
289
break;
290
}
291
}
292
293
// Redraw the page.
294
self._draw(false);
295
}
296
},
297
298
299
/**
300
* Set a list of regular expressions which blacklist URIs as not navigable
301
* via Quicksand.
302
*
303
* If a user clicks a link to one of these URIs, a normal page navigation
304
* event will occur instead of a Quicksand navigation.
305
*
306
* @param list<string> List of regular expressions.
307
* @return self
308
*/
309
setURIPatternBlacklist: function(items) {
310
var self = JX.Quicksand;
311
312
var list = [];
313
for (var ii = 0; ii < items.length; ii++) {
314
list.push(new RegExp('^' + items[ii] + '$'));
315
}
316
317
self._uriPatternBlacklist = list;
318
319
return self;
320
},
321
322
323
/**
324
* Test if a @{class:JX.URI} is on the URI pattern blacklist.
325
*
326
* @param JX.URI URI to test.
327
* @return bool True if the URI is on the blacklist.
328
*/
329
_isURIOnBlacklist: function(uri) {
330
var self = JX.Quicksand;
331
var list = self._uriPatternBlacklist;
332
333
var path = uri.getPath();
334
for (var ii = 0; ii < list.length; ii++) {
335
if (list[ii].test(path)) {
336
return true;
337
}
338
}
339
340
return false;
341
}
342
343
}
344
345
});
346
347