UNPKG

@aneves/react-flyout

Version:
413 lines (335 loc) 16.1 kB
import React from 'react'; import ReactDOM from 'react-dom'; class Flyout extends React.Component { constructor(props) { super(props); this.body = document.querySelector('body'); this.mutation = null; this.mediaQueries = this._getMediaQueries(); this.classes = { trigger: 'flyout__trigger--active', body: 'has-flyout--fixed' } // binds this._sizeHandler = this._sizeHandler.bind(this); } componentDidMount() { // console.info('flyout - componentDidMount'); this._resizeEventAdd(); this._triggerClassSet(); this._scrollPositionSave(); this._setMaxHeight(); this._setAlignment(); this._sizeHandler(); this._mutationObserve(); } componentDidUpdate() { // console.info('flyout - componentDidUpdate'); this._setAlignment(); } componentWillUnmount() { // console.info('flyout - componentWillUnmount'); this._resizeEventRemove(); this._triggerClassUnset(); this._bodyClassUnset(); this._scrollPositionLoad(); this._mutationDisconnect(); } render() { // console.info('flyout - render'); const classes = this._getClasses(); const arrow = this.props.options.type === 'tooltip' ? <span className="flyout__arrow" /> : null; return ( <div id={this.props.id} className={classes}> <div className="flyout__wrapper"> {React.cloneElement(this.props.children, {data: this.props})} </div> {arrow} </div> ); } _setAlignment(alignment) { // console.info('flyout - _setAlignment'); const dom = ReactDOM.findDOMNode(this); const parent = dom.parentNode; const flyout = document.querySelector('#'+ this.props.id); const margin = this._getMargin(); let alignments = []; if (typeof alignment === 'undefined') alignment = this._getAlignment(); alignments[0] = { 'top': - dom.offsetHeight - margin + 'px', 'right': parent.offsetWidth + margin + 'px', 'bottom': parent.offsetHeight + margin + 'px', 'left': - dom.offsetWidth - margin + 'px' } alignments[1] = { 'top': - dom.offsetHeight + parent.offsetHeight + 'px', 'right': 0, 'bottom': 0, 'left': 0 } // reset dom.style.top = ''; dom.style.right = ''; dom.style.bottom = ''; dom.style.left = ''; if (alignment[0] === 'top') { dom.style.top = alignments[0]['top']; } else if (alignment[0] === 'right') { dom.style.left = alignments[0]['right']; } else if (alignment[0] === 'bottom') { dom.style.top = alignments[0]['bottom']; } else if (alignment[0] === 'left') { dom.style.left = alignments[0]['left']; } if (alignment[1] === 'top') { dom.style.top = alignments[1]['top']; } else if (alignment[1] === 'right') { dom.style.left = alignments[1]['right']; } else if (alignment[1] === 'bottom') { dom.style.top = alignments[1]['bottom']; } else if (alignment[1] === 'left') { dom.style.right = alignments[1]['left']; } else if (alignment[1] === 'middle') { if (['top', 'bottom'].indexOf(alignment[0]) +1) { dom.style.right = - (flyout.offsetWidth/2 - parent.offsetWidth/2) + 'px'; } else { dom.style.top = - (flyout.offsetHeight/2 - parent.offsetHeight/2) + 'px'; } } // arrow if (this.props.options.type === 'tooltip') { const arrow = document.querySelector(`#${this.props.id} .flyout__arrow`); const arrowBorderWidth = parseInt(window.getComputedStyle(arrow, null).getPropertyValue('border-top-width'), 10); let arrowAlignment; arrow.style.top = 'auto'; arrow.style.right = 'auto'; arrow.style.bottom = 'auto'; arrow.style.left = 'auto'; const arrowAlignmentM = flyout.offsetWidth / 2 - arrowBorderWidth + 'px'; if (alignment[1] === 'middle' && alignment[0] === 'top') arrowAlignment = {top: '100%', left: arrowAlignmentM}; if (alignment[1] === 'middle' && alignment[0] === 'bottom') arrowAlignment = {bottom: '100%', left: arrowAlignmentM}; if (alignment[1] === 'middle' && alignment[0] === 'right') arrowAlignment = {right: '100%', top: arrowAlignmentM}; if (alignment[1] === 'middle' && alignment[0] === 'left') arrowAlignment = {left: '100%', top: arrowAlignmentM}; const arrowAlignmentTB = parent.offsetWidth / 2 - arrowBorderWidth + 'px'; if (alignment[0] === 'top' && alignment[1] === 'right') arrowAlignment = {top: '100%', left: arrowAlignmentTB}; if (alignment[0] === 'top' && alignment[1] === 'left') arrowAlignment = {top: '100%', right: arrowAlignmentTB}; if (alignment[0] === 'bottom' && alignment[1] === 'right') arrowAlignment = {bottom: '100%', left: arrowAlignmentTB}; if (alignment[0] === 'bottom' && alignment[1] === 'left') arrowAlignment = {bottom: '100%', right: arrowAlignmentTB}; const arrowAlignmentRL = parent.offsetHeight / 2 - arrowBorderWidth + 'px'; if (alignment[0] === 'right' && alignment[1] === 'top') arrowAlignment = {right: '100%', bottom: arrowAlignmentRL}; if (alignment[0] === 'right' && alignment[1] === 'bottom') arrowAlignment = {right: '100%', top: arrowAlignmentRL}; if (alignment[0] === 'left' && alignment[1] === 'top') arrowAlignment = {left: '100%', bottom: arrowAlignmentRL}; if (alignment[0] === 'left' && alignment[1] === 'bottom') arrowAlignment = {left: '100%', top: arrowAlignmentRL}; for (let k in arrowAlignment) { arrow.style[k] = arrowAlignment[k]; } } // VERIFY IF FIXED PARENT this._verifyPosition(); } _verifyPosition() { if (!this.props.options.fixed) return false; // console.info('flyout - _verifyPosition'); // if a flyout as a parent with position fixed // the only way to show all the information is to contain the flyout inside the window // to achieve this we need: // 1 - make sure the flyout's contained in the window // 2 - if not, determine the best alignment // 3 - inside scroll will take care of the rest const windowHeight = window.innerHeight; const trigger = this._getTrigger(); const triggerHeight = parseInt(trigger.offsetHeight, 10); const triggerOffsetTop = parseInt(this._getOffset(trigger)['top'], 10); const flyout = document.querySelector('#'+ this.props.id); const flyoutContent = document.querySelector('#'+ this.props.id + '> div'); // todo: fix me const flyoutHeight = parseInt(flyout.offsetHeight, 10); const flyoutOffsetTop = parseInt(this._getOffset(flyout)['top'], 10); const flyoutMinHeight = parseInt(flyout.style.minHeight, 10) | 0; const flyoutMaxHeight = parseInt(flyout.style.maxHeight, 10) | 0; const alignment = this._getAlignment(); const moreSpaceAbove = (triggerOffsetTop + (triggerHeight / 2) > (windowHeight / 2)); const getMaxHeightOffsetPosition = moreSpaceAbove ? 'top' : 'bottom'; let newFlyoutMaxHeight; // first we'll check if we need more space if (flyoutHeight + flyoutOffsetTop + this._getMaxHeightOffset(getMaxHeightOffsetPosition) > windowHeight) { // console.info('flyout - vertically out of bounds'); // now we'll verify the best vertical alignment // by finding out the optimal position, top or bottom if (moreSpaceAbove) { // more space above // console.info('flyout - we have more space above, overriding position and max-height'); // re-align to trigger flyout.style.bottom = triggerHeight; flyout.style.top = 'initial'; // let's check the max-height possible newFlyoutMaxHeight = triggerOffsetTop + triggerHeight - this._getMaxHeightOffset(getMaxHeightOffsetPosition); flyoutContent.style.maxHeight = newFlyoutMaxHeight +'px'; // let's add a class for possible customizations when forced position is applied if (alignment[0] === 'bottom' || alignment[1] === 'bottom') flyout.classList.add('flyout--forced-top'); } else { // more space bellow // console.info('flyout - we have more space bellow, overriding position and max-height'); // check the max-height possible let newFlyoutMaxHeight = windowHeight - (flyoutOffsetTop) - this._getMaxHeightOffset(getMaxHeightOffsetPosition); // verify it against a possible setted max-height if (flyoutMaxHeight !== 0 && newFlyoutMaxHeight >= flyoutMaxHeight) return false; // removing min-height for extreme cases if (newFlyoutMaxHeight < flyoutMinHeight) flyout.style.minHeight = 0; // set new max-height flyoutContent.style.maxHeight = newFlyoutMaxHeight +'px'; // let's add a class for possible customizations when forced position is applied if (alignment[0] === 'top' || alignment[1] === 'top') flyout.classList.add('flyout--forced-bottom'); } } } _setMaxHeight() { // console.info('flyout - _setMaxHeight'); const windowHeight = window.innerHeight; const flyoutContent = document.querySelector('#'+ this.props.id +' > div'); const maxHeight = parseInt(windowHeight / 1.20, 10); flyoutContent.style.maxHeight = maxHeight; } _mutationObserve() { // console.info('flyout - _mutationObserve'); const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; const flyout = document.querySelector('#'+ this.props.id); this.mutation = new MutationObserver((mutations) => { this._mutationObserved(); }); this.mutation.observe(flyout, { childList: true, subtree: true, attributes: true, characterData: true }); } _mutationObserved() { // console.info('flyout - _mutationObserved'); this._verifyPosition(); } _mutationDisconnect() { // console.info('flyout - _mutationDisconnect'); if (this.mutation) this.mutation.disconnect(); } _sizeHandler() { if (!this.props.options.mobile) return false; // console.info('flyout - _sizeHandler'); const flyout = document.querySelector('#'+ this.props.id); if (window.innerWidth < this.mediaQueries.breakpointSmall) { flyout.classList.add('flyout--fixed'); this._bodyClassSet(); } else { flyout.classList.remove('flyout--fixed'); this._bodyClassUnset(); } } _resizeEventAdd() { if (!this.props.options.mobile) return false; setTimeout(() => { // console.info('flyout - _resizeEventAdd'); window.addEventListener('resize', this._sizeHandler); }, 0); } _resizeEventRemove() { if (!this.props.options.mobile) return false; // console.info('flyout - _resizeEventRemove'); window.removeEventListener('resize', this._sizeHandler); } _triggerClassSet() { // console.info('flyout - _triggerClassSet'); this._getTrigger().classList.add(this.classes.trigger); } _triggerClassUnset() { // console.info('flyout - _triggetClassUnset'); this._getTrigger().classList.remove(this.classes.trigger); } _bodyClassSet() { // console.info('flyout - _bodyClassSet'); this.body.classList.add(this.classes.body); } _bodyClassUnset() { // console.info('flyout - _bodyClassUnset'); this.body.classList.remove(this.classes.body); } _scrollPositionSave() { // console.info('flyout - _saveScrollPosition'); // triggers active class must be use since the open event of the new flyouts // runs before the close event of the previous flyout if (document.querySelectorAll('.'+ this.classes.trigger).length) { this.body.setAttribute('data-flyoutBodyScrollPosition', window.pageYOffset); } } _scrollPositionLoad() { // console.info('flyout - _loadScrollPosition'); if (this.body.classList && !this.body.classList.contains('has-flyout--fixed')) return false; setTimeout(() => { window.scrollTo(window.pageYOffset, this.body.getAttribute('data-flyoutbodyscrollposition')); }, 0); } _getAlignment() { // console.info('flyout - _getAlignment'); const defaults = this.props.options.type === 'tooltip' ? 'top middle' : 'bottom right'; const sep = ' '; let alignment = this.props.options.align; if (typeof alignment === 'undefined') { return defaults.split(sep); } else { alignment = this.props.options.align.split(sep); return alignment.length === 2 ? alignment : defaults.split(sep); } } _getMargin() { const def = 1; const type = this.props.options.type; const margins = { tooltip: 6, menu: 0 } return typeof margins[type] !== 'undefined' ? margins[type] : def; } _getMaxHeightOffset(position) { // console.info('flyout - _getMaxHeightOffset:', position); return (position === 'top') ? 60 : 25; } _getTrigger() { // console.info('flyout - _getTrigger); const triggers = document.querySelectorAll('[data-flyout-id]'); const triggersLength = triggers.length; for (let i = 0; i < triggersLength; i++) { if (triggers[i].getAttribute('data-flyout-id') === this.props.id) { return triggers[i]; } } return null; } _getMediaQueries() { // console.info('flyout - _getMediaQueries'); const mqs = this.props.mediaQueries; let mediaQueries = {}; mediaQueries.breakpointSmall = mqs && mqs.breakpointSmall ? mqs.breakpointSmall : 640; mediaQueries.breakpointMedium = mqs && mqs.breakpointMedium ? mqs.breakpointMedium : 1024; mediaQueries.breakpointLarge = mqs && mqs.breakpointLarge ? mqs.breakpointLarge : 1920; mediaQueries.mediumUp = mqs && mqs.mediumUp ? mqs.mediumUp : 641; mediaQueries.largeUp = mqs && mqs.largeUp ? mqs.largeUp : 1025; return mediaQueries; } _getOffset(el) { // console.info('flyout - _getOffset'); const rect = el.getBoundingClientRect(); return { top: rect.top, left: rect.left }; } _getClasses() { const classes = []; classes.push(this.props.id); classes.push('flyout'); classes.push(this.props.options.type ? 'flyout--'+ this.props.options.type : 'flyout--dropdown'); classes.push('flyout--'+ this._getAlignment().join('-')); if (this.props.options.dropdownIconsLeft) classes.push('flyout--dropdown-has-icons-left'); if (this.props.options.dropdownIconsRight) classes.push('flyout--dropdown-has-icons-right'); if (this.props.options.type !== 'tooltip') classes.push(this.props.options.theme ? 'flyout--'+ this.props.options.theme : 'flyout--light'); return classes.join(' '); } }; export default Flyout;