|
import Position from './position'; |
|
import { ANIMATION_DURATION_MS, CLASS_DRIVER_HIGHLIGHTED_ELEMENT } from '../common/constants'; |
|
|
|
|
|
|
|
|
|
|
|
export default class Element { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor({ |
|
node, |
|
options, |
|
popover, |
|
stage, |
|
overlay, |
|
window, |
|
document, |
|
} = {}) { |
|
this.node = node; |
|
this.document = document; |
|
this.window = window; |
|
this.options = options; |
|
this.overlay = overlay; |
|
this.popover = popover; |
|
this.stage = stage; |
|
|
|
this.animationTimeout = null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
getScreenCoordinates() { |
|
let tempNode = this.node; |
|
|
|
let x = this.document.documentElement.offsetLeft; |
|
let y = this.document.documentElement.offsetTop; |
|
|
|
if (tempNode.offsetParent) { |
|
do { |
|
x += tempNode.offsetLeft; |
|
y += tempNode.offsetTop; |
|
} while (tempNode = tempNode.offsetParent); |
|
} |
|
|
|
return { x, y }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
isInView() { |
|
let top = this.node.offsetTop; |
|
let left = this.node.offsetLeft; |
|
const width = this.node.offsetWidth; |
|
const height = this.node.offsetHeight; |
|
|
|
let el = this.node; |
|
|
|
while (el.offsetParent) { |
|
el = el.offsetParent; |
|
top += el.offsetTop; |
|
left += el.offsetLeft; |
|
} |
|
|
|
return ( |
|
top >= this.window.pageYOffset && |
|
left >= this.window.pageXOffset && |
|
(top + height) <= (this.window.pageYOffset + this.window.innerHeight) && |
|
(left + width) <= (this.window.pageXOffset + this.window.innerWidth) |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
scrollManually() { |
|
const elementRect = this.node.getBoundingClientRect(); |
|
const absoluteElementTop = elementRect.top + this.window.pageYOffset; |
|
const middle = absoluteElementTop - (this.window.innerHeight / 2); |
|
|
|
this.window.scrollTo(0, middle); |
|
} |
|
|
|
|
|
|
|
|
|
bringInView() { |
|
if (this.isInView()) { |
|
return; |
|
} |
|
|
|
|
|
if (!this.node.scrollIntoView) { |
|
this.scrollManually(); |
|
return; |
|
} |
|
|
|
try { |
|
this.node.scrollIntoView(this.options.scrollIntoViewOptions || { |
|
behavior: 'smooth', |
|
block: 'center', |
|
}); |
|
} catch (e) { |
|
|
|
this.scrollManually(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
getCalculatedPosition() { |
|
const coordinates = this.getScreenCoordinates(); |
|
const position = new Position({ |
|
left: Number.MAX_VALUE, |
|
top: Number.MAX_VALUE, |
|
right: 0, |
|
bottom: 0, |
|
}); |
|
|
|
|
|
|
|
if (typeof coordinates.x === 'number' && typeof coordinates.y === 'number' && (this.node.offsetWidth > 0 || this.node.offsetHeight > 0)) { |
|
position.left = Math.min(position.left, coordinates.x); |
|
position.top = Math.min(position.top, coordinates.y); |
|
position.right = Math.max(position.right, coordinates.x + this.node.offsetWidth); |
|
position.bottom = Math.max(position.bottom, coordinates.y + this.node.offsetHeight); |
|
} |
|
|
|
return position; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onDeselected(hideStage = false) { |
|
this.hidePopover(); |
|
|
|
if (hideStage) { |
|
this.hideStage(); |
|
} |
|
|
|
this.node.classList.remove(CLASS_DRIVER_HIGHLIGHTED_ELEMENT); |
|
|
|
|
|
this.window.clearTimeout(this.animationTimeout); |
|
|
|
if (this.options.onDeselected) { |
|
this.options.onDeselected(this); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
isSame(element) { |
|
if (!element || !element.node) { |
|
return false; |
|
} |
|
|
|
return element.node === this.node; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
onHighlightStarted() { |
|
if (this.options.onHighlightStarted) { |
|
this.options.onHighlightStarted(this); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
onHighlighted() { |
|
this.showPopover(); |
|
this.showStage(); |
|
|
|
this.node.classList.add(CLASS_DRIVER_HIGHLIGHTED_ELEMENT); |
|
|
|
const highlightedElement = this; |
|
const popoverElement = this.popover; |
|
|
|
if (popoverElement && !popoverElement.isInView()) { |
|
popoverElement.bringInView(); |
|
} |
|
|
|
if (!highlightedElement.isInView()) { |
|
highlightedElement.bringInView(); |
|
} |
|
|
|
if (this.options.onHighlighted) { |
|
this.options.onHighlighted(this); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
showStage() { |
|
this.stage.show(this.getCalculatedPosition()); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
getNode() { |
|
return this.node; |
|
} |
|
|
|
hideStage() { |
|
this.stage.hide(); |
|
} |
|
|
|
hidePopover() { |
|
if (!this.popover) { |
|
return; |
|
} |
|
|
|
this.popover.hide(); |
|
} |
|
|
|
|
|
|
|
|
|
showPopover() { |
|
if (!this.popover) { |
|
return; |
|
} |
|
|
|
const showAtPosition = this.getCalculatedPosition(); |
|
|
|
|
|
let showAfterMs = ANIMATION_DURATION_MS; |
|
|
|
if (!this.options.animate || !this.overlay.lastHighlightedElement) { |
|
showAfterMs = 0; |
|
} |
|
|
|
this.animationTimeout = this.window.setTimeout(() => { |
|
this.popover.show(showAtPosition); |
|
}, showAfterMs); |
|
} |
|
|
|
|
|
|
|
|
|
getFullPageSize() { |
|
|
|
const body = this.document.body; |
|
const html = this.document.documentElement; |
|
|
|
return { |
|
height: Math.max(body.scrollHeight, body.offsetHeight, html.scrollHeight, html.offsetHeight), |
|
width: Math.max(body.scrollWidth, body.offsetWidth, html.scrollWidth, html.offsetWidth), |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
getSize() { |
|
return { |
|
height: Math.max(this.node.scrollHeight, this.node.offsetHeight), |
|
width: Math.max(this.node.scrollWidth, this.node.offsetWidth), |
|
}; |
|
} |
|
} |
|
|