|  | /** | 
|  | * @license | 
|  | * Copyright (C) 2020 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. | 
|  | */ | 
|  | import '../../../styles/shared-styles.js'; | 
|  | import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; | 
|  | import {getRootElement} from '../../../scripts/rootElement.js'; | 
|  |  | 
|  | const HOVER_CLASS = 'hovered'; | 
|  | const HIDE_CLASS = 'hide'; | 
|  |  | 
|  | /** | 
|  | * 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; | 
|  |  | 
|  | /** | 
|  | * How long should be wait before showing the hovercard when the user hovers | 
|  | * over the element? | 
|  | */ | 
|  | const SHOW_DELAY_MS = 500; | 
|  |  | 
|  | /** | 
|  | * The mixin for gr-hovercard-behavior. | 
|  | * | 
|  | * @example | 
|  | * | 
|  | * // LegacyElementMixin is still needed to support the old lifecycles | 
|  | * // TODO: Replace old life cycles with new ones. | 
|  | * | 
|  | * class YourComponent extends hovercardBehaviorMixin( | 
|  | *  LegacyElementMixin(PolymerElement) | 
|  | * ) { | 
|  | *   static get is() { return ''; } | 
|  | *   static get template() { return html``; } | 
|  | * } | 
|  | * | 
|  | * customElements.define(GrHovercard.is, GrHovercard); | 
|  | * | 
|  | * @see gr-hovercard.js | 
|  | * | 
|  | * // following annotations are required for polylint | 
|  | * @polymer | 
|  | * @mixinFunction | 
|  | */ | 
|  | export const hovercardBehaviorMixin = superClass => class extends superClass { | 
|  | static get properties() { | 
|  | return { | 
|  | /** | 
|  | * @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: 'right', | 
|  | }, | 
|  |  | 
|  | container: Object, | 
|  | /** | 
|  | * ID for the container element. | 
|  | * | 
|  | * @type {string} | 
|  | */ | 
|  | containerId: { | 
|  | type: String, | 
|  | value: 'gr-hovercard-container', | 
|  | }, | 
|  | }; | 
|  | } | 
|  |  | 
|  | /** @override */ | 
|  | attached() { | 
|  | super.attached(); | 
|  | if (!this._target) { this._target = this.target; } | 
|  | this.listen(this._target, 'mouseenter', 'showDelayed'); | 
|  | this.listen(this._target, 'focus', 'showDelayed'); | 
|  | this.listen(this._target, 'mouseleave', 'hide'); | 
|  | this.listen(this._target, 'blur', 'hide'); | 
|  | this.listen(this._target, 'click', 'hide'); | 
|  | } | 
|  |  | 
|  | /** @override */ | 
|  | created() { | 
|  | super.created(); | 
|  | this.addEventListener('mouseleave', | 
|  | e => this.hide(e)); | 
|  | } | 
|  |  | 
|  | /** @override */ | 
|  | ready() { | 
|  | super.ready(); | 
|  | // First, check to see if the container has already been created. | 
|  | this.container = 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); | 
|  | 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, 'click', 'hide'); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the target element that the hovercard is anchored to (the `id` of | 
|  | * the `for` property). | 
|  | * | 
|  | * @type {HTMLElement} | 
|  | */ | 
|  | get target() { | 
|  | const parentNode = dom(this).parentNode; | 
|  | // If the parentNode is a document fragment, then we need to use the host. | 
|  | const ownerRoot = dom(this).getOwnerRoot(); | 
|  | let target; | 
|  | if (this.for) { | 
|  | target = 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} opt_e DOM Event (e.g. `mouseleave` event) | 
|  | */ | 
|  | hide(opt_e) { | 
|  | this._isScheduledToShow = false; | 
|  | if (!this._isShowing) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // If 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 (opt_e) { | 
|  | if (opt_e.toElement === this || | 
|  | (opt_e.fromElement === this && opt_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.$.container.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 with a fixed delay. | 
|  | */ | 
|  | showDelayed() { | 
|  | this.showDelayedBy(SHOW_DELAY_MS); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Shows/opens the hovercard with the given delay. | 
|  | */ | 
|  | showDelayedBy(delayMs) { | 
|  | if (this._isShowing || this._isScheduledToShow) return; | 
|  | this._isScheduledToShow = true; | 
|  | setTimeout(() => { | 
|  | // This happens when the mouse leaves the target before the delay is over. | 
|  | if (!this._isScheduledToShow) return; | 
|  | this._isScheduledToShow = false; | 
|  | this.show(); | 
|  | }, delayMs); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Shows/opens the hovercard. This occurs when the user triggers the | 
|  | * `mousenter` event on the hovercard's `target` element. | 
|  | */ | 
|  | show() { | 
|  | 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); | 
|  | // We temporarily hide the hovercard until we have found the correct | 
|  | // position for it. | 
|  | this.classList.add(HIDE_CLASS); | 
|  | this.classList.add(HOVER_CLASS); | 
|  | this.updatePosition(); | 
|  | this.classList.remove(HIDE_CLASS); | 
|  | } | 
|  |  | 
|  | updatePosition() { | 
|  | const positionsToTry = new Set( | 
|  | [this.position, 'right', 'bottom-right', 'top-right', | 
|  | 'bottom', 'top', 'bottom-left', 'top-left', 'left']); | 
|  | for (const position of positionsToTry) { | 
|  | this.updatePositionTo(position); | 
|  | if (this._isInsideViewport()) return; | 
|  | } | 
|  | console.warn('Could not find a visible position for the hovercard.'); | 
|  | } | 
|  |  | 
|  | _isInsideViewport() { | 
|  | const thisRect = this.getBoundingClientRect(); | 
|  | if (thisRect.top < 0) return false; | 
|  | if (thisRect.left < 0) return false; | 
|  | const docuRect = document.documentElement.getBoundingClientRect(); | 
|  | if (thisRect.bottom > docuRect.height) return false; | 
|  | if (thisRect.right > docuRect.width) return false; | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Updates the hovercard's position based 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). | 
|  | */ | 
|  | updatePositionTo(position) { | 
|  | if (!this._target) { return; } | 
|  |  | 
|  | // Make sure that thisRect will not get any paddings and such included | 
|  | // in the width and height of the bounding client rect. | 
|  | this.style.cssText = ''; | 
|  |  | 
|  | const docuRect = document.documentElement.getBoundingClientRect(); | 
|  | const targetRect = this._target.getBoundingClientRect(); | 
|  | const thisRect = this.getBoundingClientRect(); | 
|  |  | 
|  | const targetLeft = targetRect.left - docuRect.left; | 
|  | const targetTop = targetRect.top - docuRect.top; | 
|  |  | 
|  | let hovercardLeft; | 
|  | let hovercardTop; | 
|  | const diagonalPadding = this.offset + DIAGONAL_OVERFLOW; | 
|  | let cssText = ''; | 
|  |  | 
|  | switch (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 = targetLeft + targetRect.width + 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 = targetLeft + targetRect.width + this.offset; | 
|  | hovercardTop = targetTop + 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 = targetLeft - thisRect.width - this.offset; | 
|  | hovercardTop = targetTop + 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 = targetLeft - thisRect.width - this.offset; | 
|  | hovercardTop = targetTop - 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 = targetLeft + targetRect.width + this.offset; | 
|  | hovercardTop = targetTop - 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; | 
|  | } | 
|  |  | 
|  | 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; | 
|  | } | 
|  | }; |