1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-29 02:02:41 +01:00

Restructure Hovercards to support more context information

Summary:
Ref T13602. Currently, Hovercards are functions only of the object they represent (and the viewer, etc).

Recent changes to how users who can't see an object are rendered motivate making them a function of both the object they represent //and// the context in which they are being viewed. In particular, this enables a hovecard for a user to explain "This user can't see the thing you're lookign at right now.", so visual "exiled" markers can have a path forward toward discovery.

Test Plan:
  - This change isn't expected to affect any behavior.
  - Viewed hovercards, moused over/out, resized windows, viewed standalone cards, viewed debug cards, saw no behavioral changes.

Maniphest Tasks: T13602

Differential Revision: https://secure.phabricator.com/D21553
This commit is contained in:
epriestley 2021-02-13 11:23:54 -08:00
parent 58bbd6ee88
commit 2aac3156f7
8 changed files with 349 additions and 242 deletions

View file

@ -10,7 +10,7 @@ return array(
'conpherence.pkg.css' => '0e3cf785', 'conpherence.pkg.css' => '0e3cf785',
'conpherence.pkg.js' => '020aebcf', 'conpherence.pkg.js' => '020aebcf',
'core.pkg.css' => '970b3ceb', 'core.pkg.css' => '970b3ceb',
'core.pkg.js' => 'adc34883', 'core.pkg.js' => '2fe70e3d',
'dark-console.pkg.js' => '187792c2', 'dark-console.pkg.js' => '187792c2',
'differential.pkg.css' => '5c459f92', 'differential.pkg.css' => '5c459f92',
'differential.pkg.js' => '5080baf4', 'differential.pkg.js' => '5080baf4',
@ -460,7 +460,8 @@ return array(
'rsrc/js/core/DraggableList.js' => '0169e425', 'rsrc/js/core/DraggableList.js' => '0169e425',
'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/Favicon.js' => '7930776a',
'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/FileUpload.js' => 'ab85e184',
'rsrc/js/core/Hovercard.js' => '074f0783', 'rsrc/js/core/Hovercard.js' => 'd9d29a5f',
'rsrc/js/core/HovercardList.js' => '10a5f4bf',
'rsrc/js/core/KeyboardShortcut.js' => '1a844c06', 'rsrc/js/core/KeyboardShortcut.js' => '1a844c06',
'rsrc/js/core/KeyboardShortcutManager.js' => '81debc48', 'rsrc/js/core/KeyboardShortcutManager.js' => '81debc48',
'rsrc/js/core/MultirowRowManager.js' => '5b54c823', 'rsrc/js/core/MultirowRowManager.js' => '5b54c823',
@ -485,7 +486,7 @@ return array(
'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a', 'rsrc/js/core/behavior-global-drag-and-drop.js' => '1cab0e9a',
'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b', 'rsrc/js/core/behavior-high-security-warning.js' => 'dae2d55b',
'rsrc/js/core/behavior-history-install.js' => '6a1583a8', 'rsrc/js/core/behavior-history-install.js' => '6a1583a8',
'rsrc/js/core/behavior-hovercard.js' => '6c379000', 'rsrc/js/core/behavior-hovercard.js' => '3f446c72',
'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731', 'rsrc/js/core/behavior-keyboard-pager.js' => '1325b731',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '42c44e8b', 'rsrc/js/core/behavior-keyboard-shortcuts.js' => '42c44e8b',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf', 'rsrc/js/core/behavior-lightbox-attachments.js' => 'c7e748bf',
@ -670,7 +671,7 @@ return array(
'javelin-behavior-pholio-mock-view' => '5aa1544e', 'javelin-behavior-pholio-mock-view' => '5aa1544e',
'javelin-behavior-phui-dropdown-menu' => '5cf0501a', 'javelin-behavior-phui-dropdown-menu' => '5cf0501a',
'javelin-behavior-phui-file-upload' => 'e150bd50', 'javelin-behavior-phui-file-upload' => 'e150bd50',
'javelin-behavior-phui-hovercards' => '6c379000', 'javelin-behavior-phui-hovercards' => '3f446c72',
'javelin-behavior-phui-selectable-list' => 'b26a41e4', 'javelin-behavior-phui-selectable-list' => 'b26a41e4',
'javelin-behavior-phui-submenu' => 'b5e9bff9', 'javelin-behavior-phui-submenu' => 'b5e9bff9',
'javelin-behavior-phui-tab-group' => '242aa08b', 'javelin-behavior-phui-tab-group' => '242aa08b',
@ -858,7 +859,8 @@ return array(
'phui-formation-view-css' => 'd2dec8ed', 'phui-formation-view-css' => 'd2dec8ed',
'phui-head-thing-view-css' => 'd7f293df', 'phui-head-thing-view-css' => 'd7f293df',
'phui-header-view-css' => '36c86a58', 'phui-header-view-css' => '36c86a58',
'phui-hovercard' => '074f0783', 'phui-hovercard' => 'd9d29a5f',
'phui-hovercard-list' => '10a5f4bf',
'phui-hovercard-view-css' => '6ca90fa0', 'phui-hovercard-view-css' => '6ca90fa0',
'phui-icon-set-selector-css' => '7aa5f3ec', 'phui-icon-set-selector-css' => '7aa5f3ec',
'phui-icon-view-css' => '4cbc684a', 'phui-icon-view-css' => '4cbc684a',
@ -986,13 +988,6 @@ return array(
'javelin-uri', 'javelin-uri',
'phabricator-notification', 'phabricator-notification',
), ),
'074f0783' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
),
'0889b835' => array( '0889b835' => array(
'javelin-install', 'javelin-install',
'javelin-event', 'javelin-event',
@ -1030,6 +1025,14 @@ return array(
'javelin-workflow', 'javelin-workflow',
'phuix-icon-view', 'phuix-icon-view',
), ),
'10a5f4bf' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
'phui-hovercard',
),
'111bfd2d' => array( '111bfd2d' => array(
'javelin-install', 'javelin-install',
), ),
@ -1266,6 +1269,14 @@ return array(
'phabricator-drag-and-drop-file-upload', 'phabricator-drag-and-drop-file-upload',
'phabricator-draggable-list', 'phabricator-draggable-list',
), ),
'3f446c72' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'phui-hovercard',
'phui-hovercard-list',
),
'407ee861' => array( '407ee861' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-uri', 'javelin-uri',
@ -1557,13 +1568,6 @@ return array(
'javelin-workflow', 'javelin-workflow',
'javelin-magical-init', 'javelin-magical-init',
), ),
'6c379000' => array(
'javelin-behavior',
'javelin-behavior-device',
'javelin-stratcom',
'javelin-vector',
'phui-hovercard',
),
'6cfa0008' => array( '6cfa0008' => array(
'javelin-dom', 'javelin-dom',
'javelin-dynval', 'javelin-dynval',
@ -2132,6 +2136,13 @@ return array(
'javelin-util', 'javelin-util',
'phabricator-shaped-request', 'phabricator-shaped-request',
), ),
'd9d29a5f' => array(
'javelin-install',
'javelin-dom',
'javelin-vector',
'javelin-request',
'javelin-uri',
),
'da15d3dc' => array( 'da15d3dc' => array(
'phui-oi-list-view-css', 'phui-oi-list-view-css',
), ),
@ -2367,6 +2378,7 @@ return array(
'javelin-behavior-global-drag-and-drop', 'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content', 'javelin-behavior-phabricator-reveal-content',
'phui-hovercard', 'phui-hovercard',
'phui-hovercard-list',
'javelin-behavior-phui-hovercards', 'javelin-behavior-phui-hovercards',
'javelin-color', 'javelin-color',
'javelin-fx', 'javelin-fx',

View file

@ -60,6 +60,7 @@ return array(
'javelin-behavior-global-drag-and-drop', 'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content', 'javelin-behavior-phabricator-reveal-content',
'phui-hovercard', 'phui-hovercard',
'phui-hovercard-list',
'javelin-behavior-phui-hovercards', 'javelin-behavior-phui-hovercards',
'javelin-color', 'javelin-color',
'javelin-fx', 'javelin-fx',

View file

@ -224,6 +224,43 @@ final class AphrontRequest extends Phobject {
} }
/**
* @task data
*/
public function getJSONMap($name, $default = array()) {
if (!isset($this->requestData[$name])) {
return $default;
}
$raw_data = phutil_string_cast($this->requestData[$name]);
$raw_data = trim($raw_data);
if (!strlen($raw_data)) {
return $default;
}
if ($raw_data[0] !== '{') {
throw new Exception(
pht(
'Request parameter "%s" is not formatted properly. Expected a '.
'JSON object, but value does not start with "{".',
$name));
}
try {
$json_object = phutil_json_decode($raw_data);
} catch (PhutilJSONParserException $ex) {
throw new Exception(
pht(
'Request parameter "%s" is not formatted properly. Expected a '.
'JSON object, but encountered a syntax error: %s.',
$name,
$ex->getMessage()));
}
return $json_object;
}
/** /**
* @task data * @task data
*/ */

View file

@ -9,7 +9,8 @@ final class PhabricatorSearchHovercardController
public function handleRequest(AphrontRequest $request) { public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$phids = $request->getArr('phids');
$cards = $request->getJSONMap('cards');
// If object names are provided, look them up and pretend they were // If object names are provided, look them up and pretend they were
// passed as additional PHIDs. This is primarily useful for debugging, // passed as additional PHIDs. This is primarily useful for debugging,
@ -23,18 +24,29 @@ final class PhabricatorSearchHovercardController
->execute(); ->execute();
foreach ($named_objects as $object) { foreach ($named_objects as $object) {
$phids[] = $object->getPHID(); $cards[] = array(
'objectPHID' => $object->getPHID(),
);
} }
} }
$object_phids = array();
$handle_phids = array();
foreach ($cards as $card) {
$object_phid = idx($card, 'objectPHID');
$handle_phids[] = $object_phid;
$object_phids[] = $object_phid;
}
$handles = id(new PhabricatorHandleQuery()) $handles = id(new PhabricatorHandleQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs($phids) ->withPHIDs($handle_phids)
->execute(); ->execute();
$objects = id(new PhabricatorObjectQuery()) $objects = id(new PhabricatorObjectQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs($phids) ->withPHIDs($object_phids)
->execute(); ->execute();
$objects = mpull($objects, null, 'getPHID'); $objects = mpull($objects, null, 'getPHID');
@ -67,10 +79,12 @@ final class PhabricatorSearchHovercardController
array_select_keys($objects, $extension_phids)); array_select_keys($objects, $extension_phids));
} }
$cards = array(); $results = array();
foreach ($phids as $phid) { foreach ($cards as $card_key => $card) {
$handle = $handles[$phid]; $object_phid = $card['objectPHID'];
$object = idx($objects, $phid);
$handle = $handles[$object_phid];
$object = idx($objects, $object_phid);
$hovercard = id(new PHUIHovercardView()) $hovercard = id(new PHUIHovercardView())
->setUser($viewer) ->setUser($viewer)
@ -90,18 +104,18 @@ final class PhabricatorSearchHovercardController
} }
} }
$cards[$phid] = $hovercard; $results[$card_key] = $hovercard;
} }
if ($request->isAjax()) { if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent( return id(new AphrontAjaxResponse())->setContent(
array( array(
'cards' => $cards, 'cards' => $results,
)); ));
} }
foreach ($cards as $key => $hovercard) { foreach ($results as $key => $hovercard) {
$cards[$key] = phutil_tag('div', $results[$key] = phutil_tag('div',
array( array(
'class' => 'ml', 'class' => 'ml',
), ),
@ -109,7 +123,7 @@ final class PhabricatorSearchHovercardController
} }
return $this->newPage() return $this->newPage()
->appendChild($cards) ->appendChild($results)
->setShowFooter(false); ->setShowFooter(false);
} }

View file

@ -42,7 +42,7 @@ final class PhabricatorSystemDebugUIEventListener
$submenu[] = id(new PhabricatorActionView()) $submenu[] = id(new PhabricatorActionView())
->setIcon('fa-address-card-o') ->setIcon('fa-address-card-o')
->setName(pht('View Hovercard')) ->setName(pht('View Hovercard'))
->setHref(urisprintf('/search/hovercard/?phids[]=%s', $phid)); ->setHref(urisprintf('/search/hovercard/?names=%s', $phid));
$developer_action = id(new PhabricatorActionView()) $developer_action = id(new PhabricatorActionView())
->setName(pht('Advanced/Developer...')) ->setName(pht('Advanced/Developer...'))

View file

@ -10,163 +10,18 @@
JX.install('Hovercard', { JX.install('Hovercard', {
statics : { properties: {
_node : null, hovercardKey: null,
_activeRoot : null, objectPHID: null,
_visiblePHID : null, isLoading: false,
_alignment: null, isLoaded: false,
content: null
fetchUrl : '/search/hovercard/',
/**
* Hovercard storage. {"PHID-XXXX-YYYY":"<...>", ...}
*/
_cards : {},
getAnchor : function() {
return this._activeRoot;
}, },
getCard : function() { members: {
var self = JX.Hovercard; newContentNode: function() {
return self._node; return JX.$H(this.getContent());
},
getAlignment: function() {
var self = JX.Hovercard;
return self._alignment;
},
show : function(root, phid) {
var self = JX.Hovercard;
if (root === this._activeRoot) {
return;
}
self.hide();
self._visiblePHID = phid;
self._activeRoot = root;
if (!(phid in self._cards)) {
self._load([phid]);
} else {
self._drawCard(phid);
}
},
_drawCard : function(phid) {
var self = JX.Hovercard;
// card is loading...
if (self._cards[phid] === true) {
return;
}
// Not the current requested card
if (phid != self._visiblePHID) {
return;
}
// Not loaded
if (!(phid in self._cards)) {
return;
}
var root = self._activeRoot;
var node = JX.$N('div',
{ className: 'jx-hovercard-container' },
JX.$H(self._cards[phid]));
self._node = node;
// 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);
var v = JX.Vector.getViewport();
var s = JX.Vector.getScroll();
// Move the tip so it's nicely aligned.
var margin = 20;
// Try to align the card directly above the link, with left borders
// touching.
var x = p.x;
// If this would push us off the right side of the viewport, push things
// back to the left.
if ((x + n.x + margin) > (s.x + v.x)) {
x = (s.x + v.x) - n.x - margin;
}
// Try to put the card above the link.
var y = p.y - n.y - margin;
self._alignment = 'north';
// If the card is near the top of the window, show it beneath the
// link we're hovering over instead.
if ((y - margin) < s.y) {
y = p.y + d.y + margin;
self._alignment = 'south';
}
node.style.left = x + 'px';
node.style.top = y + 'px';
},
hide : function() {
var self = JX.Hovercard;
self._visiblePHID = null;
self._activeRoot = null;
if (self._node) {
JX.DOM.remove(self._node);
self._node = null;
}
},
/**
* Pass it an array of phids to load them into storage
*
* @param list phids
*/
_load : function(phids) {
var self = JX.Hovercard;
var uri = JX.$U(self.fetchUrl);
var send = false;
for (var ii = 0; ii < phids.length; ii++) {
var phid = phids[ii];
if (phid in self._cards) {
continue;
}
self._cards[phid] = true; // means "loading"
uri.setQueryParam('phids['+ii+']', phids[ii]);
send = true;
}
if (!send) {
// already loaded / loading everything!
return;
}
new JX.Request(uri, function(r) {
for (var phid in r.cards) {
self._cards[phid] = r.cards[phid];
// Don't draw if the user is faster than the browser
// Only draw if the user is still requesting the original card
if (self.getCard() && phid != self._visiblePHID) {
continue;
}
self._drawCard(phid);
}
}).send();
} }
} }
}); });

View file

@ -0,0 +1,226 @@
/**
* @requires javelin-install
* javelin-dom
* javelin-vector
* javelin-request
* javelin-uri
* phui-hovercard
* @provides phui-hovercard-list
* @javelin
*/
JX.install('HovercardList', {
construct: function() {
this._cards = {};
this._drawRequest = {};
},
members: {
_cardNode: null,
_rootNode: null,
_cards: null,
_drawRequest: null,
_visibleCard: null,
_fetchURI : '/search/hovercard/',
getCard: function(spec) {
var hovercard_key = this._newHovercardKey(spec);
if (!(hovercard_key in this._cards)) {
var card = new JX.Hovercard()
.setHovercardKey(hovercard_key)
.setObjectPHID(spec.hoverPHID);
this._cards[hovercard_key] = card;
}
return this._cards[hovercard_key];
},
drawCard: function(card, node) {
this._drawRequest = {
card: card,
node: node
};
if (card.getIsLoaded()) {
return this._paintCard(card);
}
if (card.getIsLoading()) {
return;
}
var hovercard_key = card.getHovercardKey();
var request = {};
request[hovercard_key] = this._newCardRequest(card);
request = JX.JSON.stringify(request);
var uri = JX.$U(this._fetchURI)
.setQueryParam('cards', request);
var onresponse = JX.bind(this, function(r) {
var card = this._cards[hovercard_key];
this._fillCard(card, r.cards[hovercard_key]);
this._paintCard(card);
});
card.setIsLoading(true);
new JX.Request(uri, onresponse)
.send();
},
_newHovercardKey: function(spec) {
return 'phid=' + spec.hoverPHID;
},
_newCardRequest: function(card) {
return {
objectPHID: card.getObjectPHID()
};
},
_getCardNode: function() {
if (!this._cardNode) {
var attributes = {
className: 'jx-hovercard-container'
};
this._cardNode = JX.$N('div', attributes);
}
return this._cardNode;
},
_fillCard: function(card, response) {
card.setContent(response);
card.setIsLoaded(true);
},
_paintCard: function(card) {
var request = this._drawRequest;
if (request.card !== card) {
// This paint request is no longer the most recent paint request.
return;
}
this.hideCard();
this._rootNode = request.node;
var root = this._rootNode;
var node = this._getCardNode();
JX.DOM.setContent(node, card.newContentNode());
// 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);
var v = JX.Vector.getViewport();
var s = JX.Vector.getScroll();
// Move the tip so it's nicely aligned.
var margin = 20;
// Try to align the card directly above the link, with left borders
// touching.
var x = p.x;
// If this would push us off the right side of the viewport, push things
// back to the left.
if ((x + n.x + margin) > (s.x + v.x)) {
x = (s.x + v.x) - n.x - margin;
}
// Try to put the card above the link.
var y = p.y - n.y - margin;
var alignment = 'north';
// If the card is near the top of the window, show it beneath the
// link we're hovering over instead.
if ((y - margin) < s.y) {
y = p.y + d.y + margin;
alignment = 'south';
}
this._alignment = alignment;
node.style.left = x + 'px';
node.style.top = y + 'px';
this._visibleCard = card;
},
hideCard: function() {
var node = this._getCardNode();
JX.DOM.remove(node);
this._rootNode = null;
this._alignment = null;
this._visibleCard = null;
},
onMouseMove: function(e) {
if (!this._visibleCard) {
return;
}
var root = this._rootNode;
var node = this._getCardNode();
var alignment = this._alignment;
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;
if (alignment === 'south') {
// Cursor is below the node.
if (mouse.y > node_pos.y + node_dim.y + margin) {
this.hideCard();
}
// Cursor is above the root.
if (mouse.y < root_pos.y - margin) {
this.hideCard();
}
} else {
// Cursor is above the node.
if (mouse.y < node_pos.y - margin) {
this.hideCard();
}
// Cursor is below the root.
if (mouse.y > root_pos.y + root_dim.y + margin) {
this.hideCard();
}
}
// Cursor is too far to the left.
if (mouse.x < Math.min(root_pos.x, node_pos.x) - margin) {
this.hideCard();
}
// 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) {
this.hideCard();
}
}
}
});

View file

@ -5,10 +5,18 @@
* javelin-stratcom * javelin-stratcom
* javelin-vector * javelin-vector
* phui-hovercard * phui-hovercard
* phui-hovercard-list
* @javelin * @javelin
*/ */
JX.behavior('phui-hovercards', function() { JX.behavior('phui-hovercards', function(config, statics) {
if (statics.hovercardList) {
return;
}
var cards = new JX.HovercardList();
statics.hovercardList = cards;
// We listen for mousemove instead of mouseover to handle the case when user // We listen for mousemove instead of mouseover to handle the case when user
// scrolls with keyboard. We don't want to display hovercard if node gets // scrolls with keyboard. We don't want to display hovercard if node gets
@ -23,65 +31,19 @@ JX.behavior('phui-hovercards', function() {
return; return;
} }
var node = e.getNode('hovercard');
var data = e.getNodeData('hovercard'); var data = e.getNodeData('hovercard');
JX.Hovercard.show( var card = cards.getCard(data);
e.getNode('hovercard'),
data.hoverPHID); cards.drawCard(card, node);
}); });
JX.Stratcom.listen( JX.Stratcom.listen(
'mousemove', 'mousemove',
null, null,
function (e) { function (e) {
if (!JX.Hovercard.getCard()) { cards.onMouseMove(e);
return;
}
var root = JX.Hovercard.getAnchor();
var node = JX.Hovercard.getCard();
var align = JX.Hovercard.getAlignment();
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;
if (align == 'south') {
// Cursor is below the node.
if (mouse.y > node_pos.y + node_dim.y + margin) {
JX.Hovercard.hide();
}
// Cursor is above the root.
if (mouse.y < root_pos.y - margin) {
JX.Hovercard.hide();
}
} else {
// 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, // When we leave the page, hide any visible hovercards. If we don't do this,
@ -91,7 +53,7 @@ JX.behavior('phui-hovercards', function() {
['unload', 'onresize'], ['unload', 'onresize'],
null, null,
function() { function() {
JX.Hovercard.hide(); cards.hideCard();
}); });
}); });