mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-27 16:00:59 +01:00
5c71da8cdb
Summary: Ref T2086. Ref T7014. With the persistent column, there is significant value in retaining chrome state through navigation events, because the user may have a lot of state in the chat window (scroll position, text selection, room juggling, partially entered text, etc). We can do this by capturing navigation events and faking them with Javascript. (This can also improve performance, albeit slightly, and I believe there are better approaches to tackle performance any problems which exist with the chrome in many cases). At Facebook, this system was "Photostream" in photos and then "Quickling" in general, and the technical cost of the system was //staggering//. I am loathe to pursue it again. However: - Browsers are less junky now, and we target a smaller set of browsers. A large part of the technical cost of Quickling was the high complexity of emulating nagivation events in IE, where we needed to navigate a hidden iframe to make history entries. All desktop browsers which we might want to use this system on support the History API (although this prototype does not yet implement it). - Javelin and Phabricator's architecture are much cleaner than Facebook's was. A large part of the technical cost of Quickling was inconsistency, inlined `onclick` handlers, and general lack of coordination and abstraction. We will have //some// of this, but "correctly written" behaviors are mostly immune to it by design, and many of Javelin's architectural decisions were influenced by desire to avoid issues we encountered building this stuff for Facebook. - Some of the primitives which Quickling required (like loading resources over Ajax) have existed in a stable state in our codebase for a year or more, and adoption of these primitives was trivial and uneventful (vs a huge production at Facebook). - My hubris is bolstered by recent success with WebSockets and JX.Scrollbar, both of which I would have assessed as infeasibly complex to develop in this project a few years ago. To these points, the developer cost to prototype Photostream was several weeks; the developer cost to prototype this was a bit less than an hour. It is plausible to me that implementing and maintaining this system really will be hundreds of times less complex than it was at Facebook. Test Plan: My plan for this and D11497 is: - Get them in master. - Some secret key / relatively-hidden preference activates the column. - Quicksand activates //only// when the column is open. - We can use column + quicksand for a long period of time (i.e., over the course of Conpherence v2 development) and hammer out the long tail of issues. - When it derps up, you just hide the column and you're good to go. Reviewers: btrahan, chad Reviewed By: chad Subscribers: epriestley Maniphest Tasks: T2086, T7014 Differential Revision: https://secure.phabricator.com/D11507
226 lines
6.7 KiB
JavaScript
226 lines
6.7 KiB
JavaScript
/**
|
|
* @requires javelin-stratcom
|
|
* javelin-install
|
|
* javelin-uri
|
|
* javelin-util
|
|
* @provides javelin-history
|
|
* @javelin
|
|
*/
|
|
|
|
/**
|
|
* JX.History provides a stable interface for managing the browser's history
|
|
* stack. Whenever the history stack mutates, the "history:change" event is
|
|
* invoked via JX.Stratcom.
|
|
*
|
|
* Inspired by History Manager implemented by Christoph Pojer (@cpojer)
|
|
* @see https://github.com/cpojer/mootools-history
|
|
*/
|
|
JX.install('History', {
|
|
|
|
statics : {
|
|
|
|
// Mechanisms to @{JX.History.install} with (in preferred support order).
|
|
// The default behavior is to use the best supported mechanism.
|
|
DEFAULT : Infinity,
|
|
PUSHSTATE : 3,
|
|
HASHCHANGE : 2,
|
|
POLLING : 1,
|
|
|
|
// Last path parsed from the URL fragment.
|
|
_hash : null,
|
|
|
|
// Some browsers fire an extra "popstate" on initial page load, so we keep
|
|
// track of the initial path to normalize behavior (and not fire the extra
|
|
// event).
|
|
_initialPath : null,
|
|
|
|
// Mechanism used to interface with the browser history stack.
|
|
_mechanism : null,
|
|
|
|
/**
|
|
* Starts history management. This method must be invoked first before any
|
|
* other JX.History method can be used.
|
|
*
|
|
* @param int An optional mechanism used to interface with the browser
|
|
* history stack. If it is not supported, the next supported
|
|
* mechanism will be used.
|
|
*/
|
|
install : function(mechanism) {
|
|
if (__DEV__) {
|
|
if (JX.History._installed) {
|
|
JX.$E('JX.History.install(): can only install once.');
|
|
}
|
|
JX.History._installed = true;
|
|
}
|
|
|
|
mechanism = mechanism || JX.History.DEFAULT;
|
|
|
|
if (mechanism >= JX.History.PUSHSTATE && 'pushState' in history) {
|
|
JX.History._mechanism = JX.History.PUSHSTATE;
|
|
JX.History._initialPath = JX.History._getBasePath(location.href);
|
|
JX.Stratcom.listen('popstate', null, JX.History._handleChange);
|
|
} else if (mechanism >= JX.History.HASHCHANGE &&
|
|
'onhashchange' in window) {
|
|
JX.History._mechanism = JX.History.HASHCHANGE;
|
|
JX.Stratcom.listen('hashchange', null, JX.History._handleChange);
|
|
} else {
|
|
JX.History._mechanism = JX.History.POLLING;
|
|
setInterval(JX.History._handleChange, 200);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the name of the mechanism used to interface with the browser
|
|
* history stack.
|
|
*
|
|
* @return string Mechanism, either pushstate, hashchange, or polling.
|
|
*/
|
|
getMechanism : function() {
|
|
if (__DEV__) {
|
|
if (!JX.History._installed) {
|
|
JX.$E(
|
|
'JX.History.getMechanism(): ' +
|
|
'must call JX.History.install() first.');
|
|
}
|
|
}
|
|
return JX.History._mechanism;
|
|
},
|
|
|
|
/**
|
|
* Returns the path on top of the history stack.
|
|
*
|
|
* If the HTML5 History API is unavailable and an eligible path exists in
|
|
* the current URL fragment, the fragment is parsed for a path. Otherwise,
|
|
* the current URL path is returned.
|
|
*
|
|
* @return string Path on top of the history stack.
|
|
*/
|
|
getPath : function() {
|
|
if (__DEV__) {
|
|
if (!JX.History._installed) {
|
|
JX.$E(
|
|
'JX.History.getPath(): ' +
|
|
'must call JX.History.install() first.');
|
|
}
|
|
}
|
|
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
|
|
return JX.History._getBasePath(location.href);
|
|
} else {
|
|
var parsed = JX.History._parseFragment(location.hash);
|
|
return parsed || JX.History._getBasePath(location.href);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Pushes a path onto the history stack.
|
|
*
|
|
* @param string Path.
|
|
* @param wild State object for History API.
|
|
* @return void
|
|
*/
|
|
push : function(path, state) {
|
|
if (__DEV__) {
|
|
if (!JX.History._installed) {
|
|
JX.$E(
|
|
'JX.History.push(): ' +
|
|
'must call JX.History.install() first.');
|
|
}
|
|
}
|
|
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
|
|
if (JX.History._initialPath && JX.History._initialPath !== path) {
|
|
JX.History._initialPath = null;
|
|
}
|
|
history.pushState(state || null, null, path);
|
|
JX.History._fire(path);
|
|
} else {
|
|
location.hash = JX.History._composeFragment(path);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Modifies the path on top of the history stack.
|
|
*
|
|
* @param string Path.
|
|
* @return void
|
|
*/
|
|
replace : function(path) {
|
|
if (__DEV__) {
|
|
if (!JX.History._installed) {
|
|
JX.$E(
|
|
'JX.History.replace(): ' +
|
|
'must call JX.History.install() first.');
|
|
}
|
|
}
|
|
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
|
|
history.replaceState(null, null, path);
|
|
JX.History._fire(path);
|
|
} else {
|
|
var uri = JX.$U(location.href);
|
|
uri.setFragment(JX.History._composeFragment(path));
|
|
// Safari bug: "location.replace" does not respect changes made via
|
|
// setting "location.hash", so use "history.replaceState" if possible.
|
|
if ('replaceState' in history) {
|
|
history.replaceState(null, null, uri.toString());
|
|
JX.History._handleChange();
|
|
} else {
|
|
location.replace(uri.toString());
|
|
}
|
|
}
|
|
},
|
|
|
|
_handleChange : function(e) {
|
|
var path = JX.History.getPath();
|
|
var state = (e && e.getRawEvent().state);
|
|
|
|
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
|
|
if (path === JX.History._initialPath) {
|
|
JX.History._initialPath = null;
|
|
} else {
|
|
JX.History._fire(path, state);
|
|
}
|
|
} else {
|
|
if (path !== JX.History._hash) {
|
|
JX.History._hash = path;
|
|
JX.History._fire(path);
|
|
}
|
|
}
|
|
},
|
|
|
|
_fire : function(path, state) {
|
|
JX.Stratcom.invoke('history:change', null, {
|
|
path: JX.History._getBasePath(path),
|
|
state: state
|
|
});
|
|
},
|
|
|
|
_getBasePath : function(href) {
|
|
return JX.$U(href).setProtocol(null).setDomain(null).toString();
|
|
},
|
|
|
|
_composeFragment : function(path) {
|
|
path = JX.History._getBasePath(path);
|
|
// If the URL fragment does not change, the new path will not get pushed
|
|
// onto the stack. So we alternate the hash prefix to force a new state.
|
|
if (JX.History.getPath() === path) {
|
|
var hash = location.hash;
|
|
if (hash && hash.charAt(1) === '!') {
|
|
return '~!' + path;
|
|
}
|
|
}
|
|
return '!' + path;
|
|
},
|
|
|
|
_parseFragment : function(fragment) {
|
|
if (fragment) {
|
|
if (fragment.charAt(1) === '!') {
|
|
return fragment.substr(2);
|
|
} else if (fragment.substr(1, 2) === '~!') {
|
|
return fragment.substr(3);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
});
|