-
-
Save k-fish/3f88b24499675803edefb1262621c302 to your computer and use it in GitHub Desktop.
Tooltip with basic-dropdown - scroll
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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; | |
} | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Ember from 'ember'; | |
export default Ember.Controller.extend({ | |
apiSet: Ember.computed('dropapi', function() { | |
return this.get('dropapi'); | |
}) | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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