Path: blob/master/webroot/rsrc/externals/javelin/lib/Quicksand.js
12242 views
/**1* @requires javelin-install2* @provides javelin-quicksand3* @javelin4*/56/**7* Sink into a hopeless, cold mire of limitless depth from which there is8* no escape.9*10* Captures navigation events (like clicking links and using the back button)11* and expresses them in Javascript instead, emulating complex native browser12* behaviors in a language and context ill-suited to the task.13*14* By doing this, you abandon all hope and retreat to a world devoid of light15* or goodness. However, it allows you to have persistent UI elements which are16* not disrupted by navigation. A tempting trade, surely?17*18* To cast your soul into the darkness, use:19*20* JX.Quicksand21* .setFrame(node)22* .start();23*/24JX.install('Quicksand', {2526statics: {27_id: null,28_onpage: 0,29_cursor: 0,30_current: 0,31_content: {},32_responses: {},33_history: [],34_started: false,35_frameNode: null,36_contentNode: null,37_uriPatternBlacklist: [],3839/**40* Start Quicksand, accepting a fate of eternal torment.41*/42start: function(first_response) {43var self = JX.Quicksand;44if (self._started) {45return;46}4748JX.Stratcom.listen('click', 'tag:a', self._onclick);49JX.Stratcom.listen('history:change', null, self._onchange);5051self._started = true;52var path = JX.$U(window.location).getRelativeURI();53self._id = window.history.state || 0;54var id = self._id;55self._onpage = id;56self._history.push({path: path, id: id});5758self._responses[id] = first_response;59},606162/**63* Set the frame node which Quicksand controls content for.64*/65setFrame: function(frame) {66var self = JX.Quicksand;67self._frameNode = frame;68return self;69},707172getCurrentPageID: function() {73var self = JX.Quicksand;74if (self._id === null) {75self._id = window.history.state || 0;76}77return self._id;78},7980/**81* Respond to the user clicking a link.82*83* After a long list of checks, we may capture and simulate the resulting84* navigation.85*/86_onclick: function(e) {87var self = JX.Quicksand;8889if (!self._frameNode) {90// If Quicksand has no frame, bail.91return;92}9394if (JX.Stratcom.pass()) {95// If something else handled the event, bail.96return;97}9899if (!e.isNormalClick()) {100// If this is a right-click, control click, etc., bail.101return;102}103104if (e.getNode('workflow')) {105// Because JX.Workflow also passes these events, it might still want106// the event. Don't trigger if there's a workflow node in the stack.107return;108}109110var a = e.getNode('tag:a');111var href = a.href;112if (!href || !href.length) {113// If the <a /> the user clicked has no href, or the href is empty,114// bail.115return;116}117118if (href[0] == '#') {119// If this is an anchor on the current page, bail.120return;121}122123var uri = new JX.$U(href);124var here = new JX.$U(window.location);125if (uri.getDomain() != here.getDomain()) {126// If the link is off-domain, bail.127return;128}129130if (uri.getFragment() && uri.getPath() == here.getPath()) {131// If the link has an anchor but points at the current path, bail.132// This is presumably a long-form anchor on the current page.133134// TODO: This technically gets links which change query parameters135// wrong: they are navigation events but we won't Quicksand them.136return;137}138139if (self._isURIOnBlacklist(uri)) {140// This URI is blacklisted as not navigable via Quicksand.141return;142}143144// The fate of this action is sealed. Suck it into the depths.145e.kill();146147// If we're somewhere in history (that is, the user has pressed the148// back button one or more times, putting us in a state where pressing149// the forward button would do something) and we're navigating forward,150// all the stuff ahead of us is about to become unreachable when we151// navigate. Throw it away.152var discard = (self._history.length - self._cursor) - 1;153for (var ii = 0; ii < discard; ii++) {154var obsolete = self._history.pop();155self._responses[obsolete.id] = false;156}157158// Set up the new state and fire a request to fetch the page data.159var path = JX.$U(uri).getRelativeURI();160var id = ++self._id;161162self._history.push({path: path, id: id});163JX.History.push(path, id);164165self._cursor = (self._history.length - 1);166self._responses[id] = null;167self._current = id;168169new JX.Workflow(href, {__quicksand__: true})170.setHandler(JX.bind(null, self._onresponse, id))171.start();172},173174175/**176* Receive a response from the server with page data e.g. content.177*178* Usually we'll dump it into the page, but if the user clicked very fast179* it might already be out of date.180*/181_onresponse: function(id, r) {182var self = JX.Quicksand;183184// Before possibly updating the document, check if this response is still185// relevant.186187// We don't save the new response if the user has already destroyed188// the navigation. They can do this by pressing back, then clicking189// another link before the response can load.190if (self._responses[id] === false) {191return;192}193194// Otherwise, this data is still relevant (either data on the current195// page, or data for a page that's still somewhere in history), so we196// save it.197var new_content = JX.$H(r.content).getFragment();198self._content[id] = new_content;199self._responses[id] = r;200201// If it's the current page, draw it into the browser. It might not be202// the current page if the user already clicked another link.203if (self._current == id) {204self._draw(true);205}206},207208209/**210* Draw the current page.211*212* After a navigation event or the arrival of page content, we paint it213* onto the page.214*/215_draw: function(from_server) {216var self = JX.Quicksand;217218if (self._onpage == self._current) {219// Don't bother redrawing if we're already on the current page.220return;221}222223if (!self._responses[self._current]) {224// If we don't have this page yet, we can't draw it. We'll draw it225// when it arrives.226return;227}228229// Otherwise, we're going to replace the page content. First, save the230// current page content. Modern computers have lots and lots of RAM, so231// there is no way this could ever create a problem.232var old = window.document.createDocumentFragment();233while (self._frameNode.firstChild) {234JX.DOM.appendContent(old, self._frameNode.firstChild);235}236self._content[self._onpage] = old;237238// Now, replace it with the new content.239JX.DOM.setContent(self._frameNode, self._content[self._current]);240// Let other things redraw, etc as necessary241JX.Stratcom.invoke(242'quicksand-redraw',243null,244{245newResponse: self._responses[self._current],246newResponseID: self._current,247oldResponse: self._responses[self._onpage],248oldResponseID: self._onpage,249fromServer: from_server250});251self._onpage = self._current;252253// Scroll to the top of the page and trigger any layout adjustments.254// TODO: Maybe store the scroll position?255JX.DOM.scrollToPosition(0, 0);256JX.Stratcom.invoke('resize');257},258259260/**261* Handle navigation events.262*263* In general, we're going to pull the content out of our history and dump264* it into the document.265*/266_onchange: function(e) {267var self = JX.Quicksand;268269var data = e.getData();270data.state = data.state || null;271272// Check if we're going back to the first page we started Quicksand on.273// We don't have a state value, but can look at the path.274if (data.state === null) {275if (JX.$U(window.location).getPath() == self._history[0].path) {276data.state = 0;277}278}279280// Figure out where in history the user jumped to.281if (data.state !== null) {282self._current = data.state;283284// Point the cursor at the right place in history.285for (var ii = 0; ii < self._history.length; ii++) {286if (self._history[ii].id == self._current) {287self._cursor = ii;288break;289}290}291292// Redraw the page.293self._draw(false);294}295},296297298/**299* Set a list of regular expressions which blacklist URIs as not navigable300* via Quicksand.301*302* If a user clicks a link to one of these URIs, a normal page navigation303* event will occur instead of a Quicksand navigation.304*305* @param list<string> List of regular expressions.306* @return self307*/308setURIPatternBlacklist: function(items) {309var self = JX.Quicksand;310311var list = [];312for (var ii = 0; ii < items.length; ii++) {313list.push(new RegExp('^' + items[ii] + '$'));314}315316self._uriPatternBlacklist = list;317318return self;319},320321322/**323* Test if a @{class:JX.URI} is on the URI pattern blacklist.324*325* @param JX.URI URI to test.326* @return bool True if the URI is on the blacklist.327*/328_isURIOnBlacklist: function(uri) {329var self = JX.Quicksand;330var list = self._uriPatternBlacklist;331332var path = uri.getPath();333for (var ii = 0; ii < list.length; ii++) {334if (list[ii].test(path)) {335return true;336}337}338339return false;340}341342}343344});345346347