|
import { |
|
ANIMATION_DURATION_MS, |
|
CLASS_DRIVER_HIGHLIGHTED_ELEMENT, |
|
CLASS_FIX_STACKING_CONTEXT, |
|
CLASS_POSITION_RELATIVE, |
|
} from '../common/constants'; |
|
import { getStyleProperty } from '../common/utils'; |
|
import Position from './position'; |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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.node || this.isInView()) { |
|
return; |
|
} |
|
|
|
|
|
if (!this.node.scrollIntoView) { |
|
this.scrollManually(); |
|
return; |
|
} |
|
|
|
try { |
|
this.node.scrollIntoView(this.options.scrollIntoViewOptions || { |
|
behavior: 'instant', |
|
block: 'center', |
|
}); |
|
} catch (e) { |
|
|
|
this.scrollManually(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getCalculatedPosition() { |
|
const body = this.document.body; |
|
const documentElement = this.document.documentElement; |
|
const window = this.window; |
|
|
|
const scrollTop = this.window.pageYOffset || documentElement.scrollTop || body.scrollTop; |
|
const scrollLeft = window.pageXOffset || documentElement.scrollLeft || body.scrollLeft; |
|
const elementRect = this.node.getBoundingClientRect(); |
|
|
|
return new Position({ |
|
top: elementRect.top + scrollTop, |
|
left: elementRect.left + scrollLeft, |
|
right: elementRect.left + scrollLeft + elementRect.width, |
|
bottom: elementRect.top + scrollTop + elementRect.height, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
getPopover() { |
|
return this.popover; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
onDeselected(hideStage = false) { |
|
this.hidePopover(); |
|
|
|
if (hideStage) { |
|
this.hideStage(); |
|
} |
|
|
|
this.removeHighlightClasses(); |
|
|
|
|
|
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() { |
|
const highlightedElement = this; |
|
if (!highlightedElement.isInView()) { |
|
highlightedElement.bringInView(); |
|
} |
|
|
|
|
|
|
|
|
|
this.showPopover(); |
|
this.showStage(); |
|
this.addHighlightClasses(); |
|
|
|
if (this.options.onHighlighted) { |
|
this.options.onHighlighted(this); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
removeHighlightClasses() { |
|
this.node.classList.remove(CLASS_DRIVER_HIGHLIGHTED_ELEMENT); |
|
this.node.classList.remove(CLASS_POSITION_RELATIVE); |
|
|
|
const stackFixes = this.document.querySelectorAll(`.${CLASS_FIX_STACKING_CONTEXT}`); |
|
for (let counter = 0; counter < stackFixes.length; counter++) { |
|
stackFixes[counter].classList.remove(CLASS_FIX_STACKING_CONTEXT); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
addHighlightClasses() { |
|
this.node.classList.add(CLASS_DRIVER_HIGHLIGHTED_ELEMENT); |
|
|
|
|
|
if (this.canMakeRelative()) { |
|
this.node.classList.add(CLASS_POSITION_RELATIVE); |
|
} |
|
|
|
|
|
this.fixStackingContext(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
fixStackingContext() { |
|
let parentNode = this.node.parentNode; |
|
while (parentNode) { |
|
if (!parentNode.tagName || parentNode.tagName.toLowerCase() === 'body') { |
|
break; |
|
} |
|
|
|
const zIndex = getStyleProperty(parentNode, 'z-index'); |
|
const opacity = parseFloat(getStyleProperty(parentNode, 'opacity')); |
|
const transform = getStyleProperty(parentNode, 'transform', true); |
|
const transformStyle = getStyleProperty(parentNode, 'transform-style', true); |
|
const transformBox = getStyleProperty(parentNode, 'transform-box', true); |
|
const filter = getStyleProperty(parentNode, 'filter', true); |
|
const perspective = getStyleProperty(parentNode, 'perspective', true); |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
/[0-9]+/.test(zIndex) |
|
|| opacity < 1 |
|
|| (transform && transform !== 'none') |
|
|| (transformStyle && transformStyle !== 'flat') |
|
|| (transformBox && transformBox !== 'border-box') |
|
|| (filter && filter !== 'none') |
|
|| (perspective && perspective !== 'none') |
|
) { |
|
parentNode.classList.add(CLASS_FIX_STACKING_CONTEXT); |
|
} |
|
|
|
parentNode = parentNode.parentNode; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
canMakeRelative() { |
|
const currentPosition = this.getStyleProperty('position'); |
|
const avoidPositionsList = ['absolute', 'fixed', 'relative']; |
|
|
|
|
|
|
|
return avoidPositionsList.indexOf(currentPosition) === -1; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getStyleProperty(property) { |
|
return getStyleProperty(this.node, property); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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), |
|
}; |
|
} |
|
} |
|
|