import { ANIMATION_DURATION_MS, ID_OVERLAY, OVERLAY_HTML } from '../common/constants';
|
import { createNodeFromString } from '../common/utils';
|
|
/**
|
* Responsible for overlay creation and manipulation i.e.
|
* cutting out the visible part, animating between the sections etc
|
*/
|
export default class Overlay {
|
/**
|
* @param {Object} options
|
* @param {Window} window
|
* @param {Document} document
|
*/
|
constructor(options, window, document) {
|
this.options = options;
|
|
this.highlightedElement = null; // currently highlighted dom element (instance of Element)
|
this.lastHighlightedElement = null; // element that was highlighted before current one
|
this.hideTimer = null;
|
|
this.window = window;
|
this.document = document;
|
|
this.removeNode = this.removeNode.bind(this);
|
}
|
|
/**
|
* Prepares the overlay
|
* @private
|
*/
|
attachNode() {
|
let pageOverlay = this.document.getElementById(ID_OVERLAY);
|
if (!pageOverlay) {
|
pageOverlay = createNodeFromString(OVERLAY_HTML);
|
document.body.appendChild(pageOverlay);
|
}
|
|
this.node = pageOverlay;
|
this.node.style.opacity = '0';
|
|
if (!this.options.animate) {
|
// For non-animation cases remove the overlay because we achieve this overlay by having
|
// a higher box-shadow on the stage. Why are we doing it that way? Because the stage that
|
// is shown "behind" the highlighted element to make it pop out of the screen, it introduces
|
// some stacking contexts issues. To avoid those issues we just make the stage background
|
// transparent and achieve the overlay using the shadow so to make the element below it visible
|
// through the stage even if there are stacking issues.
|
if (this.node.parentElement) {
|
this.node.parentElement.removeChild(this.node);
|
}
|
}
|
}
|
|
/**
|
* Highlights the dom element on the screen
|
* @param {Element} element
|
* @public
|
*/
|
highlight(element) {
|
if (!element || !element.node) {
|
console.warn('Invalid element to highlight. Must be an instance of `Element`');
|
return;
|
}
|
|
// If highlighted element is not changed from last time
|
if (element.isSame(this.highlightedElement)) {
|
return;
|
}
|
|
// There might be hide timer from last time
|
// which might be getting triggered
|
this.window.clearTimeout(this.hideTimer);
|
|
// Trigger the hook for highlight started
|
element.onHighlightStarted();
|
|
// Old element has been deselected
|
if (this.highlightedElement && !this.highlightedElement.isSame(this.lastHighlightedElement)) {
|
this.highlightedElement.onDeselected();
|
}
|
|
// get the position of element around which we need to draw
|
const position = element.getCalculatedPosition();
|
if (!position.canHighlight()) {
|
return;
|
}
|
|
this.lastHighlightedElement = this.highlightedElement;
|
this.highlightedElement = element;
|
|
this.show();
|
|
// Element has been highlighted
|
this.highlightedElement.onHighlighted();
|
}
|
|
/**
|
* Shows the overlay on whole screen
|
* @public
|
*/
|
show() {
|
if (this.node && this.node.parentElement) {
|
return;
|
}
|
|
this.attachNode();
|
|
window.setTimeout(() => {
|
this.node.style.opacity = `${this.options.opacity}`;
|
this.node.style.position = 'fixed';
|
this.node.style.left = '0';
|
this.node.style.top = '0';
|
this.node.style.bottom = '0';
|
this.node.style.right = '0';
|
});
|
}
|
|
/**
|
* Returns the currently selected element
|
* @returns {null|*}
|
* @public
|
*/
|
getHighlightedElement() {
|
return this.highlightedElement;
|
}
|
|
/**
|
* Gets the element that was highlighted before current element
|
* @returns {null|*}
|
* @public
|
*/
|
getLastHighlightedElement() {
|
return this.lastHighlightedElement;
|
}
|
|
/**
|
* Removes the overlay and cancel any listeners
|
* @public
|
*/
|
clear(immediate = false) {
|
// Callback for when overlay is about to be reset
|
if (this.options.onReset) {
|
this.options.onReset(this.highlightedElement);
|
}
|
|
// Deselect the highlighted element if any
|
if (this.highlightedElement) {
|
const hideStage = true;
|
this.highlightedElement.onDeselected(hideStage);
|
}
|
|
this.highlightedElement = null;
|
this.lastHighlightedElement = null;
|
|
if (!this.node) {
|
return;
|
}
|
|
// Clear any existing timers and remove node
|
this.window.clearTimeout(this.hideTimer);
|
|
if (this.options.animate && !immediate) {
|
this.node.style.opacity = '0';
|
this.hideTimer = this.window.setTimeout(this.removeNode, ANIMATION_DURATION_MS);
|
} else {
|
this.removeNode();
|
}
|
}
|
|
/**
|
* Removes the overlay node if it exists
|
* @private
|
*/
|
removeNode() {
|
if (this.node && this.node.parentElement) {
|
this.node.parentElement.removeChild(this.node);
|
}
|
}
|
|
/**
|
* Refreshes the overlay i.e. sets the size according to current window size
|
* And moves the highlight around if necessary
|
* @public
|
*/
|
refresh() {
|
// If no highlighted element, cancel the refresh
|
if (!this.highlightedElement) {
|
return;
|
}
|
|
// Reposition the stage and show popover
|
this.highlightedElement.showPopover();
|
this.highlightedElement.showStage();
|
}
|
}
|