summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
diff options
context:
space:
mode:
Diffstat (limited to 'polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js')
-rw-r--r--polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js321
1 files changed, 321 insertions, 0 deletions
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
new file mode 100644
index 0000000000..f9c1da161c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -0,0 +1,321 @@
+/**
+ * @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;
+ },
+ });
+})();