Files
gerrit/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
Thomas Draebing 6ff72df1ce Downport "Update eslint version and eslint rules"
Legacy indent rules doesn't handle all cases. As a result there are
different indents in .js files. This commit update eslint rules and add
autofix for incorrect indents. It is expected that fix should be run
after converting to class-based elements.

Change-Id: I844132280d3fcc6203777425316d8fb348e126c0
2019-12-27 11:56:58 +01:00

322 lines
11 KiB
JavaScript

/**
* @license
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
const HOVER_CLASS = 'hovered';
/**
* When the hovercard is positioned diagonally (bottom-left, bottom-right,
* top-left, or top-right), we add additional (invisible) padding so that the
* area that a user can hover over to access the hovercard is larger.
*/
const DIAGONAL_OVERFLOW = 15;
Polymer({
is: 'gr-hovercard',
properties: {
/**
* @type {?}
*/
_target: Object,
/**
* Determines whether or not the hovercard is visible.
*
* @type {boolean}
*/
_isShowing: {
type: Boolean,
value: false,
},
/**
* The `id` of the element that the hovercard is anchored to.
*
* @type {string}
*/
for: {
type: String,
observer: '_forChanged',
},
/**
* The spacing between the top of the hovercard and the element it is
* anchored to.
*
* @type {number}
*/
offset: {
type: Number,
value: 14,
},
/**
* Positions the hovercard to the top, right, bottom, left, bottom-left,
* bottom-right, top-left, or top-right of its content.
*
* @type {string}
*/
position: {
type: String,
value: 'bottom',
},
container: Object,
/**
* ID for the container element.
*
* @type {string}
*/
containerId: {
type: String,
value: 'gr-hovercard-container',
},
},
listeners: {
mouseleave: 'hide',
},
attached() {
if (!this._target) { this._target = this.target; }
this.listen(this._target, 'mouseenter', 'show');
this.listen(this._target, 'focus', 'show');
this.listen(this._target, 'mouseleave', 'hide');
this.listen(this._target, 'blur', 'hide');
this.listen(this._target, 'tap', 'hide');
},
ready() {
// First, check to see if the container has already been created.
this.container = Gerrit.getRootElement()
.querySelector('#' + this.containerId);
if (this.container) { return; }
// If it does not exist, create and initialize the hovercard container.
this.container = document.createElement('div');
this.container.setAttribute('id', this.containerId);
Gerrit.getRootElement().appendChild(this.container);
},
removeListeners() {
this.unlisten(this._target, 'mouseenter', 'show');
this.unlisten(this._target, 'focus', 'show');
this.unlisten(this._target, 'mouseleave', 'hide');
this.unlisten(this._target, 'blur', 'hide');
this.unlisten(this._target, 'tap', 'hide');
},
/**
* Returns the target element that the hovercard is anchored to (the `id` of
* the `for` property).
*
* @type {HTMLElement}
*/
get target() {
const parentNode = Polymer.dom(this).parentNode;
// If the parentNode is a document fragment, then we need to use the host.
const ownerRoot = Polymer.dom(this).getOwnerRoot();
let target;
if (this.for) {
target = Polymer.dom(ownerRoot).querySelector('#' + this.for);
} else {
target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
ownerRoot.host :
parentNode;
}
return target;
},
/**
* Hides/closes the hovercard. This occurs when the user triggers the
* `mouseleave` event on the hovercard's `target` element (as long as the
* user is not hovering over the hovercard).
*
* @param {Event} e DOM Event (e.g. `mouseleave` event)
*/
hide(e) {
const targetRect = this._target.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
y < targetRect.bottom) {
// Sometimes the hovercard itself obscures the mouse pointer, and
// that generates a mouseleave event. We don't want to hide the hovercard
// in that situation.
return;
}
// If the hovercard is already hidden or the user is now hovering over the
// hovercard or the user is returning from the hovercard but now hovering
// over the target (to stop an annoying flicker effect), just return.
if (!this._isShowing || e.toElement === this ||
(e.fromElement === this && e.toElement === this._target)) {
return;
}
// Mark that the hovercard is not visible and do not allow focusing
this._isShowing = false;
// Clear styles in preparation for the next time we need to show the card
this.classList.remove(HOVER_CLASS);
// Reset and remove the hovercard from the DOM
this.style.cssText = '';
this.$.hovercard.setAttribute('tabindex', -1);
// Remove the hovercard from the container, given that it is still a child
// of the container.
if (this.container.contains(this)) {
this.container.removeChild(this);
}
},
/**
* Shows/opens the hovercard. This occurs when the user triggers the
* `mousenter` event on the hovercard's `target` element.
*
* @param {Event} e DOM Event (e.g., `mouseenter` event)
*/
show(e) {
if (this._isShowing) {
return;
}
// Mark that the hovercard is now visible
this._isShowing = true;
this.setAttribute('tabindex', 0);
// Add it to the DOM and calculate its position
this.container.appendChild(this);
this.updatePosition();
// Trigger the transition
this.classList.add(HOVER_CLASS);
},
/**
* Updates the hovercard's position based on the `position` attribute
* and the current position of the `target` element.
*
* The hovercard is supposed to stay open if the user hovers over it.
* To keep it open when the user moves away from the target, the bounding
* rects of the target and hovercard must touch or overlap.
*
* NOTE: You do not need to directly call this method unless you need to
* update the position of the tooltip while it is already visible (the
* target element has moved and the tooltip is still open).
*/
updatePosition() {
if (!this._target) { return; }
// Calculate the necessary measurements and positions
const parentRect = document.documentElement.getBoundingClientRect();
const targetRect = this._target.getBoundingClientRect();
const thisRect = this.getBoundingClientRect();
const targetLeft = targetRect.left - parentRect.left;
const targetTop = targetRect.top - parentRect.top;
let hovercardLeft;
let hovercardTop;
const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
let cssText = '';
// Find the top and left position values based on the position attribute
// of the hovercard.
switch (this.position) {
case 'top':
hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
hovercardTop = targetTop - thisRect.height - this.offset;
cssText += `padding-bottom:${this.offset
}px; margin-bottom:-${this.offset}px;`;
break;
case 'bottom':
hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
hovercardTop = targetTop + targetRect.height + this.offset;
cssText +=
`padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
break;
case 'left':
hovercardLeft = targetLeft - thisRect.width - this.offset;
hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
cssText +=
`padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
break;
case 'right':
hovercardLeft = targetRect.right + this.offset;
hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
cssText +=
`padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
break;
case 'bottom-right':
hovercardLeft = targetRect.left + targetRect.width + this.offset;
hovercardTop = targetRect.top + targetRect.height + this.offset;
cssText += `padding-top:${diagonalPadding}px;`;
cssText += `padding-left:${diagonalPadding}px;`;
cssText += `margin-left:-${diagonalPadding}px;`;
cssText += `margin-top:-${diagonalPadding}px;`;
break;
case 'bottom-left':
hovercardLeft = targetRect.left - thisRect.width - this.offset;
hovercardTop = targetRect.top + targetRect.height + this.offset;
cssText += `padding-top:${diagonalPadding}px;`;
cssText += `padding-right:${diagonalPadding}px;`;
cssText += `margin-right:-${diagonalPadding}px;`;
cssText += `margin-top:-${diagonalPadding}px;`;
break;
case 'top-left':
hovercardLeft = targetRect.left - thisRect.width - this.offset;
hovercardTop = targetRect.top - thisRect.height - this.offset;
cssText += `padding-bottom:${diagonalPadding}px;`;
cssText += `padding-right:${diagonalPadding}px;`;
cssText += `margin-bottom:-${diagonalPadding}px;`;
cssText += `margin-right:-${diagonalPadding}px;`;
break;
case 'top-right':
hovercardLeft = targetRect.left + targetRect.width + this.offset;
hovercardTop = targetRect.top - thisRect.height - this.offset;
cssText += `padding-bottom:${diagonalPadding}px;`;
cssText += `padding-left:${diagonalPadding}px;`;
cssText += `margin-bottom:-${diagonalPadding}px;`;
cssText += `margin-left:-${diagonalPadding}px;`;
break;
}
// Prevent hovercard from appearing outside the viewport.
// TODO(kaspern): fix hovercard appearing outside viewport on bottom and
// right.
if (hovercardLeft < 0) { hovercardLeft = 0; }
if (hovercardTop < 0) { hovercardTop = 0; }
// Set the hovercard's position
cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
this.style.cssText = cssText;
},
/**
* Responds to a change in the `for` value and gets the updated `target`
* element for the hovercard.
*
* @private
*/
_forChanged() {
this._target = this.target;
},
});
})();