Skip to content

Instantly share code, notes, and snippets.

@k-fish
Forked from aaronmw/components.fb-tooltip.js
Last active June 14, 2018 18:21
Show Gist options
  • Save k-fish/3f88b24499675803edefb1262621c302 to your computer and use it in GitHub Desktop.
Save k-fish/3f88b24499675803edefb1262621c302 to your computer and use it in GitHub Desktop.
Tooltip with basic-dropdown - scroll
/**
TODO:
[ ] "OFFSET" should be replaced with computedStyles of the arrow element; margin of
the arrow will determine from how far away from the target the popover is drawn
**/
import Ember from 'ember';
const SUPPORTED_POSITIONS = {
ABOVE: 'above',
BELOW: 'below',
LEFT: 'left',
RIGHT: 'right'
};
const POSITION_OPPOSITES = {
above: 'below',
below: 'above',
left: 'right',
right: 'left'
};
const DEFAULT_POSITION = SUPPORTED_POSITIONS.ABOVE;
const DEFAULT_POSITION_PREFERENCE = [
SUPPORTED_POSITIONS.ABOVE,
SUPPORTED_POSITIONS.BELOW,
SUPPORTED_POSITIONS.LEFT,
SUPPORTED_POSITIONS.RIGHT
];
const ARROW_TIP_OFFSET = 5;
const ARROW_SIZE = 10;
const OFFSET = ARROW_TIP_OFFSET + ARROW_SIZE;
const _testPosition = function(positionName, {
viewportWidth,
viewportHeight,
popoverHeight,
popoverWidth,
targetTop,
targetLeft,
targetHeight,
targetWidth
}) {
switch(positionName) {
case 'above':
return popoverHeight <= targetTop - OFFSET;
case 'below':
return popoverHeight <= viewportHeight - (targetTop + targetHeight) - OFFSET;
case 'left':
return popoverWidth <= targetLeft - OFFSET;
case 'right':
return popoverWidth <= viewportWidth - (targetLeft + targetWidth) - OFFSET;
}
};
const _calculateHorizontalPosition = function({
targetLeft,
targetWidth,
popoverWidth,
popoverMargins,
arrowMargins,
arrowRotatedWidth,
arrowRotatedHeight,
arrowDefinedWidth,
arrowDefinedHeight
}, position) {
switch(position) {
case 'left':
return {
left: targetLeft - popoverWidth - popoverMargins.left - (arrowRotatedWidth / 2) - 2
};
case 'right':
return {
left: targetLeft + targetWidth - popoverMargins.left + (arrowRotatedWidth / 2) + 2
};
case 'above':
case 'below':
return {
left: targetLeft + (targetWidth / 2) - ((popoverWidth + popoverMargins.left + popoverMargins.right) / 2)
};
}
};
const _calculateVerticalPosition = function({
targetTop,
targetHeight,
popoverHeight,
popoverMargins,
arrowMargins,
arrowRotatedWidth,
arrowRotatedHeight
}, position) {
switch(position) {
case 'above':
return {
top: targetTop - popoverHeight - popoverMargins.top
};
case 'below':
return {
top: targetTop + targetHeight - popoverMargins.top
};
case 'left':
case 'right':
return {
top: targetTop + (targetHeight / 2) - ((popoverHeight + popoverMargins.top + popoverMargins.bottom) / 2)
};
}
};
const _calculateArrowPosition = function({
popoverMargins,
arrowMargins,
arrowRotatedWidth,
arrowRotatedHeight,
arrowDefinedWidth,
arrowDefinedHeight
}, position) {
let styleObj = {};
switch(position) {
case 'above':
case 'below':
styleObj['left'] = '50%';
styleObj['margin-left'] = arrowDefinedWidth / -2;
break;
case 'left':
case 'right':
styleObj['top'] = '50%';
styleObj['margin-top'] = arrowDefinedHeight / -2;
break;
}
switch(position) {
case 'above':
styleObj['top'] = arrowRotatedHeight * -1;
break;
case 'below':
styleObj['bottom'] = arrowRotatedHeight * -1;
break;
case 'left':
styleObj['left'] = arrowRotatedWidth * -1;
break;
case 'right':
styleObj['right'] = arrowRotatedWidth * -1;
break;
}
return Object.entries(styleObj).reduce((styleString, [propName, propValue]) => {
propValue = typeof propValue == 'string' ? propValue : `${propValue}px`;
return `${styleString}${propName}: ${propValue}; `;
}, '');
};
export default Ember.Component.extend({
didReceiveAttrs() {
this._super(...arguments);
let positionPreference = this.get('positionPreference');
if (typeof positionPreference == 'string') {
positionPreference = [positionPreference];
this.set('positionPreference', positionPreference);
}
positionPreference.forEach(function(value, index) {
if (!Object.values(SUPPORTED_POSITIONS).includes(value)) {
throw `positionPreference "${value}" is not supported`;
}
});
},
_supportedPositions: SUPPORTED_POSITIONS,
// Returns a complete list of positions ordered by user's preference
// and filled in with default order if the user supplies fewer than
// all four positions
//
// positionPreference="above"
// -> "above" "below" "left" "right"
//
// positionPreference=(array "above" "right")
// -> "above" "right" "below" "left"
_positionPreference: Ember.computed('positionPreference', function() {
const positionPreference = this.get('positionPreference');
let preferenceOrder = positionPreference;
// Add opposite positions in order
positionPreference.forEach(function(positionName, index) {
let oppositePositionName = POSITION_OPPOSITES[positionName];
if (preferenceOrder.indexOf(oppositePositionName) === -1) {
preferenceOrder.push(oppositePositionName);
}
});
// Now just fill in the missing position names based on their
// order in the default DEFAULT_POSITION_PREFERENCE list
DEFAULT_POSITION_PREFERENCE.forEach(function(positionName, index) {
if (preferenceOrder.indexOf(positionName) === -1) {
preferenceOrder.push(positionName);
}
});
return preferenceOrder;
}),
calculatePosition(options, target, popover) {
console.clear();
const targetClientBoundingRect = target.getBoundingClientRect();
const popoverClientBoundingRect = popover.getBoundingClientRect();
const {
top: targetTop,
left: targetLeft,
width: targetWidth,
height: targetHeight
} = targetClientBoundingRect;
const popoverStyles = window.getComputedStyle(popover);
const popoverMargins = {
left: parseFloat(popoverStyles.marginLeft),
right: parseFloat(popoverStyles.marginRight),
top: parseFloat(popoverStyles.marginTop),
bottom: parseFloat(popoverStyles.marginBottom)
};
const {
top: popoverTop,
left: popoverLeft,
height: popoverHeight,
width: popoverWidth
} = popoverClientBoundingRect;
const boundingSizes = {
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
popoverMargins,
targetTop,
targetLeft,
targetWidth,
targetHeight,
popoverHeight,
popoverWidth,
popoverTop,
popoverLeft
};
let position = null;
for (var i = 0; i < options.positionPreference.length; i++) {
if (_testPosition(options.positionPreference[i], boundingSizes)) {
position = options.positionPreference[i];
break;
}
};
this.set('currentPosition', position);
/**
* Arrow calculations. Needs to occur after position preference has been determined, to accurately retrieve size post rotation.
**/
const arrow = target.querySelector(`.popover-arrow--${position}`);
debugger;
const arrowClientBoundingRect = arrow.getBoundingClientRect();
const arrowStyles = window.getComputedStyle(arrow);
const arrowBorderWidths = {
horizontal: parseFloat(arrowStyles.borderRightWidth),
vertical: parseFloat(arrowStyles.borderTopWidth)
};
const arrowMargins = {
left: parseFloat(arrowStyles.marginLeft),
right: parseFloat(arrowStyles.marginRight),
top: parseFloat(arrowStyles.marginTop),
bottom: parseFloat(arrowStyles.marginBottom)
};
/*
This is the height of the arrow's box after it's been rotated. So an arrow
with a width and height of 15px will actually be 17px tall and wide when rotated.
It's the size of the box that contains the "diamond" shape of the arrow.
*/
const {
width: arrowRotatedWidth,
height: arrowRotatedHeight
} = arrowClientBoundingRect;
const arrowSizes = {
arrowMargins,
arrowRotatedWidth: arrowRotatedWidth - arrowBorderWidths.horizontal,
arrowRotatedHeight: arrowRotatedHeight - arrowBorderWidths.vertical,
arrowDefinedWidth: arrow.clientWidth,
arrowDefinedHeight: arrow.clientHeight
};
Object.assign(boundingSizes, arrowSizes);
const horizontalPosition = _calculateHorizontalPosition(boundingSizes, position);
const verticalPosition = _calculateVerticalPosition(boundingSizes, position);
const arrowStyle = _calculateArrowPosition(boundingSizes, position);
const popoverStyle = Object.assign({}, horizontalPosition, verticalPosition);
return {
style: popoverStyle,
position,
arrowStyle
};
},
actions: {
calculatePosition(options, ...rest) {
const positionData = this.calculatePosition(options, ...rest);
this.set('arrowStyle', positionData.arrowStyle);
return positionData;
}
}
});
import Ember from 'ember';
export default Ember.Controller.extend({
apiSet: Ember.computed('dropapi', function() {
return this.get('dropapi');
})
});
html {
height: 4000px;
overflow: scroll;
}
.scrollpane {
height: 100px;
width: 400px;
border: 1px solid black;
overflow-y: scroll;
}
body {
margin: 12px 16px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 12pt;
}
.demo {
width: 200px;
margin: 0 auto;
}
.ember-basic-dropdown-content {
border: 2px solid #ccc;
border-radius: 5px;
padding: 10px;
margin: 13px 17px 23px 29px;
/**/
margin: 20px;
/**/
}
/* Superficial arrow styles */
.popover-arrow {
width: 15px;
height: 15px;
border-width: 2px;
border-radius: 4px;
border-style: solid;
border-color: #ccc;
background: white;
}
/* Mechanical arrow styles */
.popover-arrow {
position: absolute;
z-index: 100000;
border-top-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-left: 0;
border-bottom: 0;
}
.popover-arrow--above {
transform: rotate(135deg);
}
.popover-arrow--below {
transform: rotate(-45deg);
}
.popover-arrow--right {
transform: rotate(-135deg);
}
.popover-arrow--left {
transform: rotate(45deg);
}
<h1>Tooltip with basic-dropdown example</h1>
<div id="wormhole-destination">
</div>
{{!--
<div class="demo">
{{#fb-tooltip
positionPreference=(array "right")
destination="wormhole-destination"
registerAPI=(action (mut tooltipAPI))
as |tooltip|
}}
{{#tooltip.target}}
<input value="My data here">
{{/tooltip.target}}
{{#tooltip.content}}
This data is not valid. Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum Lorem Ipsum
{{/tooltip.content}}
{{/fb-tooltip}}
<br>
<br>
<button {{action tooltipAPI.actions.open}}>Display tooltip</button>
</div>
<br>
<br>
<br>
<div class="demo">
{{#fb-tooltip
destination="wormhole-destination"
positionPreference="above"
registerAPI=(action (mut secondTooltipAPI))
as |tooltip|
}}
{{#tooltip.target}}
<input value="My data here">
{{/tooltip.target}}
{{#tooltip.content}}
This data is not valid.
{{/tooltip.content}}
{{/fb-tooltip}}
<br>
<br>
<button {{action secondTooltipAPI.actions.open}}>Display tooltip above</button>
</div>
<br>
<br>
<br>
<div class="demo">
{{#fb-tooltip
destination="wormhole-destination"
positionPreference="below"
registerAPI=(action (mut thirdTooltipAPI))
as |tooltip|
}}
{{#tooltip.target}}
<input value="My data here">
{{/tooltip.target}}
{{#tooltip.content}}
This data is not valid.
{{/tooltip.content}}
{{/fb-tooltip}}
<br>
<br>
<button {{action thirdTooltipAPI.actions.open}}>Display tooltip below</button>
</div>
<br>
<br>
<br>
--}}
<div class="scrollpane">
<div class="demo">
{{#fb-tooltip
destination="wormhole-destination"
positionPreference="left"
registerAPI=(action (mut forthTooltipAPI))
as |tooltip|
}}
{{#tooltip.target}}
<input value="My data here">
{{/tooltip.target}}
{{#tooltip.content}}
This data is not valid.
{{/tooltip.content}}
{{/fb-tooltip}}
<br>
<br>
<button {{action forthTooltipAPI.actions.open}}>Display tooltip left</button>
</div>
<br>
<br>
<br>
<div class="demo">
{{#fb-tooltip
destination="wormhole-destination"
positionPreference="right"
registerAPI=(action (mut fifthTooltipAPI))
as |tooltip|
}}
{{#tooltip.target}}
<input value="My data here">
{{/tooltip.target}}
{{#tooltip.content}}
This data is not valid.
{{/tooltip.content}}
{{/fb-tooltip}}
<br>
<br>
<button {{action fifthTooltipAPI.actions.open}}>Display tooltip right</button>
</div>
</div>
{{#basic-dropdown
calculatePosition=(action 'calculatePosition' (hash positionPreference=_positionPreference))
destination="wormhole-destination"
registerAPI=registerAPI
as |dropdown|
}}
{{yield
(hash
target=(component 'fb-tooltip/tooltip-target' dropdown=dropdown tagName='' currentPosition=currentPosition arrowStyle=arrowStyle)
content=(component dropdown.content)
)
}}
{{/basic-dropdown}}
<div data-ebd-id="{{dropdown.uniqueId}}-trigger" style="position: relative; display: inline-block">
{{yield}}
{{#if dropdown.isOpen}}
<div class="popover-arrow popover-arrow--{{currentPosition}}" style="{{arrowStyle}}"></div>
<div class="popover-arrow popover-arrow--above" style="{{arrowStyle}}; {{if currentPosition 'display: none;'}}"></div>
<div class="popover-arrow popover-arrow--left" style="{{arrowStyle}}; {{if currentPosition 'display: none;'}}"></div>
<div class="popover-arrow popover-arrow--right" style="{{arrowStyle}}; {{if currentPosition 'display: none;'}}"></div>
<div class="popover-arrow popover-arrow--bottom" style="{{arrowStyle}}; {{if currentPosition 'display: none;'}}"></div>
{{/if}}
</div>
{
"version": "0.13.0",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.16.2",
"ember-template-compiler": "2.16.2",
"ember-testing": "2.16.2"
},
"addons": {
"ember-data": "2.16.3",
"ember-basic-dropdown": "0.33.0",
"ember-composable-helpers": "2.1.0",
"ember-truth-helpers": "2.0.0"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment