Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14019089
D11507.id27683.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
D11507.id27683.diff
View Options
diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -195,6 +195,7 @@
'rsrc/externals/javelin/lib/JSON.js' => '69adf288',
'rsrc/externals/javelin/lib/Leader.js' => '331b1611',
'rsrc/externals/javelin/lib/Mask.js' => '8a41885b',
+ 'rsrc/externals/javelin/lib/Quicksand.js' => 'f088b4b6',
'rsrc/externals/javelin/lib/Request.js' => '94b750d2',
'rsrc/externals/javelin/lib/Resource.js' => '44959b73',
'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692',
@@ -349,7 +350,7 @@
'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761',
'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18',
'rsrc/js/application/config/behavior-reorder-fields.js' => '14a827de',
- 'rsrc/js/application/conpherence/behavior-durable-column.js' => '3927b5aa',
+ 'rsrc/js/application/conpherence/behavior-durable-column.js' => '14bf9acd',
'rsrc/js/application/conpherence/behavior-menu.js' => 'f0a41b9f',
'rsrc/js/application/conpherence/behavior-pontificate.js' => '2f6efe18',
'rsrc/js/application/conpherence/behavior-widget-pane.js' => '40b1ff90',
@@ -582,7 +583,7 @@
'javelin-behavior-diffusion-locate-file' => '6d3e1947',
'javelin-behavior-diffusion-pull-lastmodified' => '2b228192',
'javelin-behavior-doorkeeper-tag' => 'e5822781',
- 'javelin-behavior-durable-column' => '3927b5aa',
+ 'javelin-behavior-durable-column' => '14bf9acd',
'javelin-behavior-error-log' => '6882e80a',
'javelin-behavior-fancy-datepicker' => 'c51ae228',
'javelin-behavior-global-drag-and-drop' => '07f199d8',
@@ -667,6 +668,7 @@
'javelin-leader' => '331b1611',
'javelin-magical-init' => '4df97779',
'javelin-mask' => '8a41885b',
+ 'javelin-quicksand' => 'f088b4b6',
'javelin-reactor' => '2b8de964',
'javelin-reactor-dom' => 'c90a04fc',
'javelin-reactor-node-calmer' => '76f4ebed',
@@ -917,6 +919,14 @@
'javelin-json',
'phabricator-draggable-list',
),
+ '14bf9acd' => array(
+ 'javelin-behavior',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-scrollbar',
+ 'javelin-quicksand',
+ 'phabricator-keyboard-shortcut',
+ ),
'14d7a8b8' => array(
'javelin-behavior',
'javelin-behavior-device',
@@ -1043,13 +1053,6 @@
'javelin-json',
'phabricator-prefab',
),
- '3927b5aa' => array(
- 'javelin-behavior',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-scrollbar',
- 'phabricator-keyboard-shortcut',
- ),
'3ab51e2c' => array(
'javelin-behavior',
'javelin-behavior-device',
@@ -1877,6 +1880,9 @@
'javelin-install',
'javelin-util',
),
+ 'f088b4b6' => array(
+ 'javelin-install',
+ ),
'f0a41b9f' => array(
'javelin-behavior',
'javelin-dom',
diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -19,6 +19,7 @@
const TYPE_CONTINUE = '__continue__';
const TYPE_PREVIEW = '__preview__';
const TYPE_HISEC = '__hisec__';
+ const TYPE_QUICKSAND = '__quicksand__';
private $host;
private $path;
@@ -194,10 +195,14 @@
return $this->getExists(self::TYPE_AJAX);
}
- final public function isJavelinWorkflow() {
+ final public function isWorkflow() {
return $this->getExists(self::TYPE_WORKFLOW);
}
+ final public function isQuicksand() {
+ return $this->getExists(self::TYPE_QUICKSAND);
+ }
+
final public function isConduit() {
return $this->getExists(self::TYPE_CONDUIT);
}
diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
--- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
@@ -65,7 +65,7 @@
}
// For non-workflow requests, return a Ajax response.
- if ($request->isAjax() && !$request->isJavelinWorkflow()) {
+ if ($request->isAjax() && !$request->isWorkflow()) {
// Log these; they don't get shown on the client and can be difficult
// to debug.
phlog($ex);
diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php
--- a/src/applications/base/controller/PhabricatorController.php
+++ b/src/applications/base/controller/PhabricatorController.php
@@ -242,8 +242,18 @@
public function buildStandardPageResponse($view, array $data) {
$page = $this->buildStandardPageView();
$page->appendChild($view);
- $response = new AphrontWebpageResponse();
- $response->setContent($page->render());
+ return $this->buildPageResponse($page);
+ }
+
+ private function buildPageResponse($page) {
+ if ($this->getRequest()->isQuicksand()) {
+ $response = id(new AphrontAjaxResponse())
+ ->setContent($page->renderForQuicksand());
+ } else {
+ $response = id(new AphrontWebpageResponse())
+ ->setContent($page->render());
+ }
+
return $response;
}
@@ -303,8 +313,7 @@
$page->setApplicationMenu($application_menu);
}
- $response = new AphrontWebpageResponse();
- return $response->setContent($page->render());
+ return $this->buildPageResponse($page);
}
public function didProcessRequest($response) {
@@ -331,7 +340,7 @@
}
if ($response instanceof AphrontDialogResponse) {
- if (!$request->isAjax()) {
+ if (!$request->isAjax() && !$request->isQuicksand()) {
$dialog = $response->getDialog();
$title = $dialog->getTitle();
diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php
--- a/src/view/page/PhabricatorStandardPageView.php
+++ b/src/view/page/PhabricatorStandardPageView.php
@@ -311,8 +311,6 @@
}
protected function getBody() {
- $console = $this->getConsole();
-
$user = null;
$request = $this->getRequest();
if ($request) {
@@ -367,11 +365,13 @@
$developer_warning,
$setup_warning,
$header_chrome,
- phutil_tag_div('phabricator-standard-page-body', array(
- ($console ? hsprintf('<darkconsole />') : null),
- parent::getBody(),
- $this->renderFooter(),
- )),
+ phutil_tag(
+ 'div',
+ array(
+ 'id' => 'phabricator-standard-page-body',
+ 'class' => 'phabricator-standard-page-body',
+ ),
+ $this->renderPageBodyContent()),
));
$durable_column = null;
@@ -390,6 +390,16 @@
));
}
+ private function renderPageBodyContent() {
+ $console = $this->getConsole();
+
+ return array(
+ ($console ? hsprintf('<darkconsole />') : null),
+ parent::getBody(),
+ $this->renderFooter(),
+ );
+ }
+
protected function getTail() {
$request = $this->getRequest();
$user = $request->getUser();
@@ -529,4 +539,15 @@
$foot);
}
+ public function renderForQuicksand() {
+ // TODO: We could run a lighter version of this and skip some work. In
+ // particular, we end up including many redundant resources.
+ $this->willRenderPage();
+ $response = $this->renderPageBodyContent();
+ $response = $this->willSendResponse($response);
+
+ return array(
+ 'content' => hsprintf('%s', $response),
+ );
+ }
}
diff --git a/webroot/rsrc/externals/javelin/lib/History.js b/webroot/rsrc/externals/javelin/lib/History.js
--- a/webroot/rsrc/externals/javelin/lib/History.js
+++ b/webroot/rsrc/externals/javelin/lib/History.js
@@ -115,9 +115,10 @@
* Pushes a path onto the history stack.
*
* @param string Path.
+ * @param wild State object for History API.
* @return void
*/
- push : function(path) {
+ push : function(path, state) {
if (__DEV__) {
if (!JX.History._installed) {
JX.$E(
@@ -129,7 +130,7 @@
if (JX.History._initialPath && JX.History._initialPath !== path) {
JX.History._initialPath = null;
}
- history.pushState(null, null, path);
+ history.pushState(state || null, null, path);
JX.History._fire(path);
} else {
location.hash = JX.History._composeFragment(path);
@@ -167,13 +168,15 @@
}
},
- _handleChange : function() {
+ _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);
+ JX.History._fire(path, state);
}
} else {
if (path !== JX.History._hash) {
@@ -183,9 +186,10 @@
}
},
- _fire : function(path) {
+ _fire : function(path, state) {
JX.Stratcom.invoke('history:change', null, {
- path: JX.History._getBasePath(path)
+ path: JX.History._getBasePath(path),
+ state: state
});
},
diff --git a/webroot/rsrc/externals/javelin/lib/Quicksand.js b/webroot/rsrc/externals/javelin/lib/Quicksand.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/externals/javelin/lib/Quicksand.js
@@ -0,0 +1,218 @@
+/**
+ * @requires javelin-install
+ * @provides javelin-quicksand
+ * @javelin
+ */
+
+/**
+ * Sink into a hopeless, cold mire of limitless depth from which there is
+ * no escape.
+ *
+ * Captures navigation events (like clicking links and using the back button)
+ * and expresses them in Javascript instead, emulating complex native browser
+ * behaviors in a language and context ill-suited to the task.
+ *
+ * By doing this, you abandon all hope and retreat to a world devoid of light
+ * or goodness. However, it allows you to have persistent UI elements which are
+ * not disrupted by navigation. A tempting trade, surely?
+ *
+ * To cast your soul into the darkness, use:
+ *
+ * JX.Quicksand
+ * .setFrame(node)
+ * .start();
+ */
+JX.install('Quicksand', {
+
+ statics: {
+ _id: 0,
+ _onpage: 0,
+ _cursor: 0,
+ _current: 0,
+ _content: {},
+ _history: [],
+ _started: false,
+ _frame: null,
+ _contentNode: null,
+
+ start: function() {
+ var self = JX.Quicksand;
+ if (self._started) {
+ return;
+ }
+
+ JX.Stratcom.listen('click', 'tag:a', self._onclick);
+ JX.Stratcom.listen('history:change', null, self._onchange);
+
+ self._started = true;
+ },
+
+ setFrame: function(frame) {
+ var self = JX.Quicksand;
+ self._frame = frame;
+ return self;
+ },
+
+ _onclick: function(e) {
+ var self = JX.Quicksand;
+ if (!self._frame) {
+ // If Quicksand has no frame, bail.
+ return;
+ }
+
+ if (JX.Stratcom.pass()) {
+ // If something else handled the event, bail.
+ return;
+ }
+
+ if (!e.isNormalClick()) {
+ // If this is a right-click, control click, etc., bail.
+ return;
+ }
+
+ if (e.getNode('workflow')) {
+ // Because JX.Workflow also passes these events, it might still want
+ // the event. Don't trigger if there's a workflow node in the stack.
+ return;
+ }
+
+ var a = e.getNode('tag:a');
+ var href = a.href;
+ if (!href || !href.length) {
+ // If the <a /> the user clicked has no href, or the href is empty,
+ // bail.
+ return;
+ }
+
+ if (href[0] == '#') {
+ // If this is an anchor on the current page, bail.
+ return;
+ }
+
+ var uri = new JX.$U(href);
+ var here = new JX.$U(window.location);
+ if (uri.getDomain() != here.getDomain()) {
+ // If the link is off-domain, bail.
+ return;
+ }
+
+ if (uri.getFragment() && uri.getPath() == here.getPath()) {
+ // If the link has an anchor but points at the current path, bail.
+ // This is presumably a long-form anchor on the current page.
+
+ // TODO: This technically gets links which change query parameters
+ // wrong: they are navigation events but we won't Quicksand them.
+ return;
+ }
+
+ // The fate of this action is sealed. Suck it into the depths.
+ e.kill();
+
+ // If we're somewhere in history (that is, the user has pressed the
+ // back button one or more times, putting us in a state where pressing
+ // the forward button would do something) and we're navigating forward,
+ // all the stuff ahead of us is about to become unreachable when we
+ // navigate. Throw it away.
+ var discard = (self._history.length - self._cursor) - 1;
+ for (var ii = 0; ii < discard; ii++) {
+ var obsolete = self._history.pop();
+ self._content[obsolete.id] = false;
+ }
+
+ var path = self._getPath(uri);
+ var id = ++self._id;
+
+ JX.History.push(path, id);
+
+ self._history.push({path: path, id: id});
+ self._cursor = (self._history.length - 1);
+ self._content[id] = null;
+ self._current = id;
+
+ new JX.Workflow(href, {__quicksand__: true})
+ .setHandler(JX.bind(null, self._onresponse, id, (id == 1)))
+ .start();
+ },
+
+ _onresponse: function(id, is_first, r) {
+ var self = JX.Quicksand;
+
+ // Before possibly updating the document, check if this response is still
+ // relevant.
+
+ // We don't save the new content if the user has already destroyed
+ // the navigation. They can do this by pressing back, then clicking
+ // another link before the content can load.
+ if (self._content[id] === false) {
+ return;
+ }
+
+ // Otherwise, this data is still relevant (either data on the current
+ // page, or data for a page that's still somewhere in history), so we
+ // save it.
+ var new_content = JX.$H(r.content).getFragment();
+ self._content[id] = new_content;
+
+ // If it's the current page, draw it into the browser. It might not be
+ // the current page if the user already clicked another link.
+ if (self._current == id) {
+ self._draw();
+ }
+ },
+
+ _draw: function() {
+ var self = JX.Quicksand;
+
+ if (self._onpage == self._current) {
+ // Don't bother redrawing if we're already on the current page.
+ return;
+ }
+
+ if (!self._content[self._current]) {
+ // If we don't have this page yet, we can't draw it. We'll draw it
+ // when it arrives.
+ return;
+ }
+
+ // Otherwise, we're going to replace the page content. First, save the
+ // current page content. Modern computers have lots and lots of RAM, so
+ // there is no way this could ever create a problem.
+ var old = window.document.createDocumentFragment();
+ while (self._frame.firstChild) {
+ JX.DOM.appendContent(old, self._frame.firstChild);
+ }
+ self._content[self._onpage] = old;
+
+ // Now, replace it with the new content.
+ JX.DOM.setContent(self._frame, self._content[self._current]);
+ self._onpage = self._current;
+ },
+
+ _onchange: function(e) {
+ var self = JX.Quicksand;
+
+ var data = e.getData();
+ if (data.state) {
+ self._current = data.state;
+
+ for (var ii = 0; ii < self._history.length; ii++) {
+ if (self._history[ii].id == self._current) {
+ self._cursor = ii;
+ break;
+ }
+ }
+
+ self._draw();
+ }
+ },
+
+ _getPath: function(uri) {
+ return JX.$U(uri)
+ .setProtocol(null)
+ .setPort(null)
+ .setDomain(null)
+ .toString();
+ }
+ }
+
+});
diff --git a/webroot/rsrc/externals/javelin/lib/Request.js b/webroot/rsrc/externals/javelin/lib/Request.js
--- a/webroot/rsrc/externals/javelin/lib/Request.js
+++ b/webroot/rsrc/externals/javelin/lib/Request.js
@@ -215,7 +215,7 @@
if (expect_guard && xport.responseText.indexOf('for (;;);') !== 0) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
- 'server returned an invalid response.');
+ 'server returned an invalid response: ' + xport.responseText);
}
if (expect_guard && xport.responseText == 'for (;;);') {
JX.$E(
diff --git a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js
--- a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js
+++ b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js
@@ -4,12 +4,14 @@
* javelin-dom
* javelin-stratcom
* javelin-scrollbar
+ * javelin-quicksand
* phabricator-keyboard-shortcut
*/
JX.behavior('durable-column', function() {
var frame = JX.$('phabricator-standard-page');
+ var quick = JX.$('phabricator-standard-page-body');
var show = false;
new JX.KeyboardShortcut('\\', 'Toggle Chat (ALPHA)')
@@ -18,10 +20,12 @@
JX.DOM.alterClass(frame, 'with-durable-column', show);
JX.$('durable-column').style.display = (show ? 'block' : 'none');
JX.Stratcom.invoke('resize');
+ JX.Quicksand.setFrame(show ? quick : null);
})
.register();
new JX.Scrollbar(JX.$('phui-durable-column-content'));
+ JX.Quicksand.start();
});
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Nov 6, 9:32 PM (6 d, 4 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6740185
Default Alt Text
D11507.id27683.diff (17 KB)
Attached To
Mode
D11507: Quicksand, an ignoble successor to Quickling
Attached
Detach File
Event Timeline
Log In to Comment