diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index faa6b78db9..5d4d2737b4 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1903,6 +1903,19 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/uiexample/gesture-example.js', ), + 'javelin-behavior-phabricator-hovercards' => + array( + 'uri' => '/res/8366e963/rsrc/js/application/core/behavior-hovercard.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-behavior-device', + 2 => 'javelin-stratcom', + 3 => 'phabricator-hovercard', + ), + 'disk' => '/rsrc/js/application/core/behavior-hovercard.js', + ), 'javelin-behavior-phabricator-keyboard-pager' => array( 'uri' => '/res/56d64eff/rsrc/js/application/core/behavior-keyboard-pager.js', @@ -3027,9 +3040,24 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/layout/phabricator-header-view.css', ), + 'phabricator-hovercard' => + array( + 'uri' => '/res/1db6db48/rsrc/js/application/core/Hovercard.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + 4 => 'javelin-request', + 5 => 'phabricator-busy', + ), + 'disk' => '/rsrc/js/application/core/Hovercard.js', + ), 'phabricator-hovercard-view-css' => array( - 'uri' => '/res/68d69a87/rsrc/css/layout/phabricator-hovercard-view.css', + 'uri' => '/res/ed56200c/rsrc/css/layout/phabricator-hovercard-view.css', 'type' => 'css', 'requires' => array( diff --git a/src/view/layout/PhabricatorTagView.php b/src/view/layout/PhabricatorTagView.php index b215d58a04..85b523e40b 100644 --- a/src/view/layout/PhabricatorTagView.php +++ b/src/view/layout/PhabricatorTagView.php @@ -130,6 +130,7 @@ final class PhabricatorTagView extends AphrontView { } if ($this->phid) { + Javelin::initBehavior('phabricator-hovercards'); return javelin_tag( 'a', diff --git a/webroot/rsrc/css/layout/phabricator-hovercard-view.css b/webroot/rsrc/css/layout/phabricator-hovercard-view.css index bd0539750d..5f86626bd4 100644 --- a/webroot/rsrc/css/layout/phabricator-hovercard-view.css +++ b/webroot/rsrc/css/layout/phabricator-hovercard-view.css @@ -2,10 +2,13 @@ * @provides phabricator-hovercard-view-css */ +.jx-hovercard-container { + position: absolute; +} + .phabricator-hovercard-wrapper { border-radius: 4px; width: 400px; - margin: auto; padding: 4px; background-color: #cccccc; } diff --git a/webroot/rsrc/js/application/core/Hovercard.js b/webroot/rsrc/js/application/core/Hovercard.js new file mode 100644 index 0000000000..85fdaa5cd2 --- /dev/null +++ b/webroot/rsrc/js/application/core/Hovercard.js @@ -0,0 +1,124 @@ +/** + * @requires javelin-install + * javelin-util + * javelin-dom + * javelin-vector + * javelin-request + * phabricator-busy + * @provides phabricator-hovercard + * @javelin + */ + +JX.install('Hovercard', { + + statics : { + _node : null, + _activeRoot : null, + + _didScrape : false, + + fetchUrl : '/search/hovercard/retrieve/', + + /** + * Hovercard storage. {"PHID-XXXX-YYYY":"<...>", ...} + */ + cards : {}, + + show : function(root, phid) { + + // Hovercards are all loaded by now, but when somebody previews a comment + // for example it may not be loaded yet. + if (!JX.Hovercard.cards[phid]) { + JX.Hovercard.load([phid]); + } + + var node = JX.$N('div', + { className: 'jx-hovercard-container' }, + JX.Hovercard.cards[phid]); + + JX.Hovercard.hide(); + this._node = node; + this._activeRoot = root; + + // Append the card to the document, but offscreen, so we can measure it. + node.style.left = '-10000px'; + document.body.appendChild(node); + + // Retrieve size from child (wrapper), since node gives wrong dimensions? + var child = node.firstChild; + + var p = JX.$V(root); + var d = JX.Vector.getDim(root); + var n = JX.Vector.getDim(child); + + // Move the tip so it's nicely aligned. + // I'm just doing north alignment for now + // TODO: Gracefully align to the side in edge cases + // I know, hardcoded paddings... + var x = parseInt(p.x - ((n.x - d.x) / 2)) + 20; + var y = parseInt(p.y - n.y) - 20; + + // Why use 4? Shouldn't it be just 2? + if (x < (n.x / 4)) { + x += (n.x / 4); + } + + if (y < n.y) { + // Place it at the bottom + y += n.y + d.y + 50; + } + + node.style.left = x + 'px'; + node.style.top = y + 'px'; + + }, + + hide : function() { + if (this._node) { + JX.DOM.remove(this._node); + this._node = null; + } + if (this._activeRoot) { + this._activeRoot = null; + } + }, + + /** + * Pass it an array of phids to load them into storage + * + * @param list phids + */ + load : function(phids) { + var uri = JX.$U(JX.Hovercard.fetchUrl); + + for (var ii = 0; ii < phids.length; ii++) { + uri.setQueryParam("phids["+ii+"]", phids[ii]); + } + + new JX.Request(uri, function(r) { + for (var phid in r.cards) { + JX.Hovercard.cards[phid] = JX.$H(r.cards[phid]); + } + }).send(); + }, + + // For later probably + // Currently unused + scrapeAndLoad: function() { + if (!JX.Hovercard._didScrape) { + // I assume links only for now + var cards = JX.DOM.scry(document, 'a', 'hovercard'); + var phids = []; + var data; + for (var i = 0; i < cards.length; i++) { + data = JX.Stratcom.getData(cards[i]); + phids.push(data.hoverPHID); + } + + JX.Hovercard.load(phids); + + JX.Hovercard._didScrape = true; + } + } + } +}); diff --git a/webroot/rsrc/js/application/core/behavior-hovercard.js b/webroot/rsrc/js/application/core/behavior-hovercard.js new file mode 100644 index 0000000000..e55368c875 --- /dev/null +++ b/webroot/rsrc/js/application/core/behavior-hovercard.js @@ -0,0 +1,92 @@ +/** + * @provides javelin-behavior-phabricator-hovercards + * @requires javelin-behavior + * javelin-behavior-device + * javelin-stratcom + * phabricator-hovercard + * @javelin + */ + +JX.behavior('phabricator-hovercards', function(config) { + + // First find all hovercard-able object on page and load them in a batch + JX.Hovercard.scrapeAndLoad(); + + // Event stuff + JX.Stratcom.listen( + ['mouseover'], + 'hovercard', + function (e) { + if (e.getType() == 'mouseout') { + JX.Hovercard.hide(); + return; + } + + if (JX.Device.getDevice() != 'desktop') { + return; + } + + var data = e.getNodeData('hovercard'); + + JX.Hovercard.show( + e.getNode('hovercard'), + data.hoverPHID); + }); + + JX.Stratcom.listen( + ['mousemove'], + null, + function (e) { + if (JX.Device.getDevice() != 'desktop') { + return; + } + + if (!JX.Hovercard._node) { + return; + } + + var root = JX.Hovercard._activeRoot; + var node = JX.Hovercard._node.firstChild; + + var mouse = JX.$V(e); + var node_pos = JX.$V(node); + var node_dim = JX.Vector.getDim(node); + var root_pos = JX.$V(root); + var root_dim = JX.Vector.getDim(root); + + var margin = 20; + + // Cursor is above the node. + if (mouse.y < node_pos.y - margin) { + JX.Hovercard.hide(); + } + + // Cursor is below the root. + if (mouse.y > root_pos.y + root_dim.y + margin) { + JX.Hovercard.hide(); + } + + // Cursor is too far to the left. + if (mouse.x < Math.min(root_pos.x, node_pos.x) - margin) { + JX.Hovercard.hide(); + } + + // Cursor is too far to the right. + if (mouse.x > + Math.max(root_pos.x + root_dim.x, node_pos.x + node_dim.x) + margin) { + + JX.Hovercard.hide(); + } + }); + + // When we leave the page, hide any visible hovercards. If we don't do this, + // clicking a link with a hovercard and then hitting "back" will give you a + // phantom tooltip. + JX.Stratcom.listen( + 'unload', + null, + function(e) { + JX.Hovercard.hide(); + }); + +});