| // Copyright 2009 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as VisualLogging from '../visual_logging/visual_logging.js'; |
| |
| import {GlassPane, MarginBehavior, SizeBehavior} from './GlassPane.js'; |
| import popoverStyles from './popover.css.js'; |
| |
| export class PopoverHelper { |
| static createPopover = (jslogContext?: string): GlassPane => { |
| const popover = new GlassPane(`${VisualLogging.popover(jslogContext).parent('mapped')}`); |
| popover.registerRequiredCSS(popoverStyles); |
| popover.setSizeBehavior(SizeBehavior.MEASURE_CONTENT); |
| popover.setMarginBehavior(MarginBehavior.DEFAULT_MARGIN); |
| return popover; |
| }; |
| private disableOnClick: boolean; |
| private getRequest: (arg0: MouseEvent|KeyboardEvent) => PopoverRequest | null; |
| private scheduledRequest: PopoverRequest|null; |
| private hidePopoverCallback: (() => void)|null; |
| readonly container: HTMLElement; |
| private showTimeout: number; |
| private hideTimeout: number; |
| private hidePopoverTimer: number|null; |
| private showPopoverTimer: number|null; |
| private readonly boundMouseDown: (event: MouseEvent) => void; |
| private readonly boundMouseMove: (ev: MouseEvent) => void; |
| private readonly boundMouseOut: (event: MouseEvent) => void; |
| private readonly boundScrollEnd: (event: Event) => void; |
| private readonly boundKeyUp: (ev: KeyboardEvent) => void; |
| jslogContext?: string; |
| constructor( |
| container: HTMLElement, getRequest: (arg0: MouseEvent|KeyboardEvent) => PopoverRequest | null, |
| jslogContext?: string) { |
| this.disableOnClick = false; |
| this.getRequest = getRequest; |
| this.jslogContext = jslogContext; |
| this.scheduledRequest = null; |
| this.hidePopoverCallback = null; |
| this.container = container; |
| this.showTimeout = 0; |
| this.hideTimeout = 0; |
| this.hidePopoverTimer = null; |
| this.showPopoverTimer = null; |
| this.boundMouseDown = this.mouseDown.bind(this); |
| this.boundMouseMove = this.mouseMove.bind(this); |
| this.boundMouseOut = this.mouseOut.bind(this); |
| this.boundScrollEnd = this.scrollEnd.bind(this); |
| this.boundKeyUp = this.keyUp.bind(this); |
| this.container.addEventListener('mousedown', this.boundMouseDown, false); |
| this.container.addEventListener('mousemove', this.boundMouseMove, false); |
| this.container.addEventListener('mouseout', this.boundMouseOut, false); |
| this.container.addEventListener('keyup', this.boundKeyUp, false); |
| this.setTimeout(1000); |
| } |
| |
| setTimeout(showTimeout: number, hideTimeout?: number): void { |
| this.showTimeout = showTimeout; |
| this.hideTimeout = typeof hideTimeout === 'number' ? hideTimeout : showTimeout / 2; |
| } |
| |
| setDisableOnClick(disableOnClick: boolean): void { |
| this.disableOnClick = disableOnClick; |
| } |
| |
| private eventInScheduledContent(event: MouseEvent): boolean { |
| return this.scheduledRequest ? this.scheduledRequest.box.contains(event.clientX, event.clientY) : false; |
| } |
| |
| private scrollEnd(_event: Event): void { |
| this.hidePopover(); |
| } |
| |
| private mouseDown(event: MouseEvent): void { |
| if (this.disableOnClick) { |
| this.hidePopover(); |
| return; |
| } |
| if (this.eventInScheduledContent(event)) { |
| return; |
| } |
| |
| this.startHidePopoverTimer(0); |
| this.stopShowPopoverTimer(); |
| this.startShowPopoverTimer(event, 0); |
| } |
| |
| private keyUp(event: KeyboardEvent): void { |
| if (event.altKey && event.key === 'ArrowDown') { |
| if (this.isPopoverVisible()) { |
| this.hidePopover(); |
| } else { |
| this.stopShowPopoverTimer(); |
| this.startHidePopoverTimer(0); |
| this.startShowPopoverTimer(event, 0); |
| } |
| event.stopPropagation(); |
| } else if (event.key === 'Escape' && this.isPopoverVisible()) { |
| this.hidePopover(); |
| event.stopPropagation(); |
| } |
| } |
| |
| private mouseMove(event: MouseEvent): void { |
| if (this.eventInScheduledContent(event)) { |
| // Reschedule showing popover since mouse moved and |
| // we only want to show the popover when the mouse is |
| // standing still on the container for some amount of time. |
| this.stopShowPopoverTimer(); |
| this.startShowPopoverTimer(event, this.isPopoverVisible() ? this.showTimeout * 0.6 : this.showTimeout); |
| return; |
| } |
| |
| this.startHidePopoverTimer(this.hideTimeout); |
| this.stopShowPopoverTimer(); |
| if (event.buttons && this.disableOnClick) { |
| return; |
| } |
| this.startShowPopoverTimer(event, this.isPopoverVisible() ? this.showTimeout * 0.6 : this.showTimeout); |
| } |
| |
| private popoverMouseMove(_event: Event): void { |
| this.stopHidePopoverTimer(); |
| } |
| |
| private popoverMouseOut(popover: GlassPane, event: MouseEvent): void { |
| if (!popover.isShowing()) { |
| return; |
| } |
| const node = (event.relatedTarget as Node | null); |
| if (node && !node.isSelfOrDescendant(popover.contentElement)) { |
| this.startHidePopoverTimer(this.hideTimeout); |
| } |
| } |
| |
| private mouseOut(event: MouseEvent): void { |
| if (!this.isPopoverVisible()) { |
| return; |
| } |
| if (!this.eventInScheduledContent(event)) { |
| this.startHidePopoverTimer(this.hideTimeout); |
| } |
| } |
| |
| private startHidePopoverTimer(timeout: number): void { |
| // User has |timeout| ms to reach the popup. |
| if (!this.hidePopoverCallback || this.hidePopoverTimer) { |
| return; |
| } |
| |
| this.hidePopoverTimer = window.setTimeout(() => { |
| this.#hidePopover(); |
| this.hidePopoverTimer = null; |
| }, timeout); |
| } |
| |
| private startShowPopoverTimer(event: MouseEvent|KeyboardEvent, timeout: number): void { |
| this.scheduledRequest = this.getRequest.call(null, event); |
| if (!this.scheduledRequest) { |
| return; |
| } |
| |
| this.showPopoverTimer = window.setTimeout(() => { |
| this.showPopoverTimer = null; |
| this.stopHidePopoverTimer(); |
| this.#hidePopover(); |
| const document = ((event.target as Node).ownerDocument) as Document; |
| this.showPopover(document); |
| }, timeout); |
| } |
| |
| private stopShowPopoverTimer(): void { |
| if (!this.showPopoverTimer) { |
| return; |
| } |
| clearTimeout(this.showPopoverTimer); |
| this.showPopoverTimer = null; |
| } |
| |
| isPopoverVisible(): boolean { |
| return Boolean(this.hidePopoverCallback); |
| } |
| |
| hidePopover(): void { |
| this.stopShowPopoverTimer(); |
| this.#hidePopover(); |
| } |
| |
| #hidePopover(): void { |
| if (!this.hidePopoverCallback) { |
| return; |
| } |
| this.hidePopoverCallback.call(null); |
| this.hidePopoverCallback = null; |
| } |
| |
| private showPopover(document: Document): void { |
| const popover = PopoverHelper.createPopover(this.jslogContext); |
| const request = this.scheduledRequest; |
| if (!request) { |
| return; |
| } |
| void request.show.call(null, popover).then(success => { |
| if (!success) { |
| return; |
| } |
| |
| if (this.scheduledRequest !== request) { |
| if (request.hide) { |
| request.hide.call(null); |
| } |
| return; |
| } |
| |
| // This should not happen, but we hide previous popover to be on the safe side. |
| if (popoverHelperInstance) { |
| popoverHelperInstance.hidePopover(); |
| } |
| popoverHelperInstance = this; |
| |
| VisualLogging.setMappedParent(popover.contentElement, this.container); |
| popover.contentElement.style.scrollbarGutter = 'stable'; |
| popover.contentElement.addEventListener('mousemove', this.popoverMouseMove.bind(this), true); |
| popover.contentElement.addEventListener('mouseout', this.popoverMouseOut.bind(this, popover), true); |
| popover.setContentAnchorBox(request.box); |
| popover.show(document); |
| |
| this.container.addEventListener('scrollend', this.boundScrollEnd, true); |
| |
| this.hidePopoverCallback = () => { |
| if (request.hide) { |
| request.hide.call(null); |
| } |
| popover.hide(); |
| popoverHelperInstance = null; |
| this.container.removeEventListener('scrollend', this.boundScrollEnd, true); |
| }; |
| }); |
| } |
| |
| private stopHidePopoverTimer(): void { |
| if (!this.hidePopoverTimer) { |
| return; |
| } |
| clearTimeout(this.hidePopoverTimer); |
| this.hidePopoverTimer = null; |
| |
| // We know that we reached the popup, but we might have moved over other elements. |
| // Discard pending command. |
| this.stopShowPopoverTimer(); |
| } |
| |
| dispose(): void { |
| this.container.removeEventListener('mousedown', this.boundMouseDown, false); |
| this.container.removeEventListener('mousemove', this.boundMouseMove, false); |
| this.container.removeEventListener('mouseout', this.boundMouseOut, false); |
| this.container.removeEventListener('keyup', this.boundKeyUp, false); |
| } |
| } |
| |
| let popoverHelperInstance: PopoverHelper|null = null; |
| export interface PopoverRequest { |
| box: AnchorBox; |
| show: (arg0: GlassPane) => Promise<boolean>; |
| hide?: (() => void); |
| } |