Created
May 2, 2017 23:19
-
-
Save kybishop/eb65e0ec569d42990aa93fe557d4ac3c to your computer and use it in GitHub Desktop.
Scroll issues
This file contains hidden or 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'; | |
import layout from '../templates/components/ember-attacher-inner'; | |
export default Ember.Component.extend({ | |
/** | |
* ================== PUBLIC CONFIG OPTIONS ================== | |
*/ | |
// See ember-attacher.js, which passes all the default values into this component | |
/** | |
* ================== COMPONENT LIFECYCLE HOOKS ================== | |
*/ | |
init() { | |
this._super(...arguments); | |
// Holds the current popper target so event listeners can be removed if the target changes | |
this._currentTarget = null; | |
// The debounced _hide() is stored here so it can be cancelled | |
// if a _show() is triggered before the _hide() is executed | |
this._delayedHide = null; | |
// The debounced _show() is stored here so it can be cancelled | |
// if a _hide() is triggered before the _show() is executed | |
this._delayedShow = null; | |
// The final source of truth on whether or not all _hide() or _show() actions have completed | |
this._isHidden = true; | |
// Holds a delayed function to toggle the visibility of the attachment. | |
// Used to make sure animations can complete before the attachment is hidden. | |
this._isVisibleTimeout = null; | |
this._showListenersOnTargetByEvent = {}; | |
this._hideListenersOnTargetByEvent = {}; | |
// Hacks to make sure event listeners have the right context and are still cancellable later | |
this._hideIfMouseOutsideTargetOrAttachment = | |
this._hideIfMouseOutsideTargetOrAttachment.bind(this); | |
this._debouncedHideIfMouseOutsideTargetOrAttachment = | |
this._debouncedHideIfMouseOutsideTargetOrAttachment.bind(this); | |
this._hideOnBlur = this._hideOnBlur.bind(this); | |
this._hideOnMouseLeaveTarget = this._hideOnMouseLeaveTarget.bind(this); | |
this._hideAfterDelay = this._hideAfterDelay.bind(this); | |
this._showAfterDelay = this._showAfterDelay.bind(this); | |
this._show = this._show.bind(this); | |
this._hide = this._hide.bind(this); | |
}, | |
didInsertElement() { | |
this._super(...arguments); | |
// The Popper does not exist until after the element has been inserted | |
Ember.run.next(() => { | |
this._addListenersForShowEvents(); | |
this.get('popper').disableEventListeners(); | |
}); | |
}, | |
_addListenersForShowEvents() { | |
let target = this.get('target'); | |
let showOn = this.get('_showOn'); | |
if (!target) { | |
return; | |
} | |
this._currentTarget = target; | |
showOn.forEach(event => { | |
this._showListenersOnTargetByEvent[event] = this._showAfterDelay; | |
target.addEventListener(event, this._showAfterDelay); | |
}); | |
}, | |
willDestroyElement() { | |
this._super(...arguments); | |
this._removeEventListeners(); | |
}, | |
_removeEventListeners() { | |
let target = this._currentTarget; | |
[this._hideListenersOnTargetByEvent, this._showListenersOnTargetByEvent] | |
.forEach(eventToListener => { | |
Object.keys(eventToListener).forEach(event => { | |
target.removeEventListener(event, eventToListener[event]); | |
}); | |
}); | |
}, | |
/** | |
* ================== PRIVATE IMPLEMENTATION DETAILS ================== | |
*/ | |
classNameBindings: ['_animation', '_isStartingAnimation:ember-attacher-show:ember-attacher-hide'], | |
// Part of the Component superclass. isVisible == false sets 'display: none' | |
isVisible: false, | |
layout, | |
_animation: Ember.computed('animation', function() { | |
return `ember-attacher-${this.get('animation')}`; | |
}), | |
_hideOn: Ember.computed('hideOn', function() { | |
return this.get('hideOn').split(' '); | |
}), | |
_showOn: Ember.computed('showOn', function() { | |
return this.get('showOn').split(' '); | |
}), | |
_setIsVisibleAfterDelay(isVisible, delay) { | |
Ember.run.cancel(this._isVisibleTimeout); | |
if (delay) { | |
this._isVisibleTimeout = | |
Ember.run.later(this, () => { this.set('isVisible', isVisible) }, delay); | |
} else { | |
this.set('isVisible', isVisible); | |
} | |
}, | |
_targetOrTriggersChanged: Ember.observer( | |
'hideOn', | |
'showOn', | |
'target', | |
function() { | |
this._removeEventListeners(); | |
// Regardless of whether or not the attachment is hidden, we want to add the show listeners | |
this._addListenersForShowEvents(); | |
if (!this._isHidden) { | |
this._addListenersforHideEvents(); | |
} | |
} | |
), | |
/** | |
* ================== SHOW ATTACHMENT LOGIC ================== | |
*/ | |
_showAfterDelay() { | |
Ember.run.cancel(this._delayedHide); | |
Ember.run.cancel(this._isVisibleTimeout); | |
// The attachment is already visible or the target has been destroyed | |
if (!this._isHidden || !this.get('target')) { | |
return; | |
} | |
this._addListenersforHideEvents(); | |
let showDelay = parseInt(this.get('showDelay')); | |
this._delayedShow = Ember.run.debounce(this, this._show, showDelay, !showDelay); | |
}, | |
_show() { | |
// The target of interactive tooltips receive the 'active' class | |
if (this.get('interactive')) { | |
this.get('target').classList.add('active') | |
} | |
// Make the attachment visible immediately so transition animations can take place | |
this._setIsVisibleAfterDelay(true, 0); | |
let popper = this.get('popper'); | |
popper.update(); | |
popper.enableEventListeners(); | |
// Start the show animation on the next cycle so CSS transitions can have an effect | |
// If we start the animation immediately, the transition won't work because isVisible will | |
// turn on the same time as our show animation, and `display: none` => `display: anythingElse` | |
// is not transition-able | |
Ember.run.next(this, () => { | |
this.element.style.transitionDuration = `${this.get('showDuration')}ms`; | |
this.set('_isStartingAnimation', true); | |
}) | |
this._isHidden = false; | |
}, | |
_addListenersforHideEvents() { | |
let hideOn = this.get('_hideOn'); | |
let target = this.get('target'); | |
if (hideOn.indexOf('click') !== -1) { | |
let showOnClickListener = this._showListenersOnTargetByEvent['click']; | |
if (showOnClickListener) { | |
target.removeEventListener('click', showOnClickListener); | |
delete this._showListenersOnTargetByEvent['click']; | |
} | |
this._hideListenersOnTargetByEvent['click'] = this._hideAfterDelay; | |
target.addEventListener('click', this._hideAfterDelay); | |
} | |
// Hides the attachment when the mouse leaves the target | |
// (or leaves both target and attachment for interactive attachments) | |
if (hideOn.indexOf('mouseleave') !== -1) { | |
this._hideListenersOnTargetByEvent['mouseleave'] = this._hideOnMouseLeaveTarget; | |
target.addEventListener('mouseleave', this._hideOnMouseLeaveTarget); | |
} | |
// Hides the attachment when focus is lost on the target | |
if (hideOn.indexOf('blur') !== -1) { | |
this._hideListenersOnTargetByEvent['blur'] = this._hideOnBlur; | |
target.addEventListener('blur', this._hideOnBlur); | |
} | |
}, | |
_hideOnMouseLeaveTarget() { | |
if (this.get('interactive')) { | |
// TODO(kjb) Consider storing this somewhere and removing it if onHide or target changes | |
// TODO(kjb) Should debounce this, but hiding appears sluggish if you debounce. | |
// - If you debounce with immediate fire, you get a bug where you can move out of the | |
// attachment and not trigger the hide because the hide check was debounced | |
// - Ideally we would debounce with an immediate run, then instead of debouncing, we would | |
// queue another fire at the end of the debounce period | |
document.addEventListener('mousemove', this._hideIfMouseOutsideTargetOrAttachment) | |
} else { | |
this._hideAfterDelay(); | |
} | |
}, | |
_debouncedHideIfMouseOutsideTargetOrAttachment(event) { | |
Ember.run.debounce(this, this._hideIfMouseOutsideTargetOrAttachment, event, 10) | |
}, | |
_hideIfMouseOutsideTargetOrAttachment(event) { | |
let target = this.get('target'); | |
// If cursor is not on the attachment or target, hide the element | |
if (!this.element.contains(event.target) | |
&& !target.contains(event.target) | |
// TODO(kjb) this should be optional since it is rather expensive. | |
// Maybe call it isOffsetFromTarget | |
&& !this._isCursorBetweenTargetAndAttachment(event)) { | |
// Remove this listener before hiding the attachment | |
document.removeEventListener('mousemove', this._hideIfMouseOutsideTargetOrAttachment); | |
target.classList.remove('active'); | |
this._hideAfterDelay(); | |
} | |
}, | |
_isCursorBetweenTargetAndAttachment(event) { | |
let {clientX, clientY} = event; | |
let attachmentPosition = this.element.getBoundingClientRect(); | |
let targetPosition = this.get('target').getBoundingClientRect(); | |
// Check if cursor is between a left-flipped attachment | |
if (attachmentPosition.right < targetPosition.left | |
&& clientX >= attachmentPosition.right && clientX <= targetPosition.left | |
&& clientY > Math.min(attachmentPosition.top, targetPosition.top) | |
&& clientY < Math.max(attachmentPosition.bottom, targetPosition.bottom)) { | |
return true; | |
} | |
// Check if cursor is between a right-flipped attachment | |
if (attachmentPosition.left > targetPosition.right | |
&& clientX <= attachmentPosition.left && clientX >= targetPosition.right | |
&& clientY > Math.min(attachmentPosition.top, targetPosition.top) | |
&& clientY < Math.max(attachmentPosition.bottom, targetPosition.bottom)) { | |
return true; | |
} | |
// Check if cursor is between a bottom-flipped attachment | |
if (attachmentPosition.top > targetPosition.bottom | |
&& clientY <= attachmentPosition.top && clientY >= targetPosition.bottom | |
&& clientX > Math.min(attachmentPosition.left, targetPosition.left) | |
&& clientX < Math.max(attachmentPosition.right, targetPosition.right)) { | |
return true; | |
} | |
// Check if cursor is between a top-flipped attachment | |
if (attachmentPosition.bottom < targetPosition.top | |
&& clientY >= attachmentPosition.bottom && clientY <= targetPosition.top | |
&& clientX > Math.min(attachmentPosition.left, targetPosition.left) | |
&& clientX < Math.max(attachmentPosition.right, targetPosition.right)) { | |
return true; | |
} | |
return false; | |
}, | |
_hideOnBlur(event) { | |
if (!event.relatedTarget || !this.element.contains(event.relatedTarget)) { | |
this._hideAfterDelay(); | |
} | |
}, | |
_startShowAnimation() { | |
// Start the show animation on the next cycle so CSS transitions can have an effect | |
// If we start the animation immediately, the transition won't work because isVisible will | |
// turn on the same time as our show animation, and `display: none` => `display: anythingElse` | |
// is not transition-able | |
Ember.run.next(this, () => { | |
this.element.style.transitionDuration = `${this.get('showDuration')}ms`; | |
this.set('_isStartingAnimation', true); | |
}) | |
}, | |
/** | |
* ================== HIDE ATTACHMENT LOGIC ================== | |
*/ | |
_hideAfterDelay() { | |
Ember.run.cancel(this._delayedShow); | |
Ember.run.cancel(this._isVisibleTimeout); | |
// The attachment is already hidden or the target was destroyed | |
if (this._isHidden || !this.get('target')) { | |
return; | |
} | |
let hideDelay = parseInt(this.get('hideDelay')); | |
this._delayedHide = Ember.run.debounce(this, this._hide, hideDelay, !hideDelay); | |
}, | |
_hide() { | |
this._removeListenersForHideEvents(); | |
let hideDuration = parseInt(this.get('hideDuration')); | |
this.element.style.transitionDuration = `${hideDuration}ms`; | |
this.set('_isStartingAnimation', false); | |
// Wait for any animations to complete before hiding the attachment | |
this._setIsVisibleAfterDelay(false, hideDuration); | |
this.get('popper').disableEventListeners(); | |
this._isHidden = true; | |
}, | |
_removeListenersForHideEvents() { | |
let target = this.get('target'); | |
let showOn = this.get('_showOn'); | |
// Switch clicking back to a show event | |
if (showOn.indexOf('click') !== -1) { | |
let hideOnClickListener = this._hideListenersOnTargetByEvent['click']; | |
if (hideOnClickListener) { | |
target.removeEventListener('click', hideOnClickListener); | |
delete this._hideListenersOnTargetByEvent['click']; | |
} | |
this._showListenersOnTargetByEvent['click'] = this._showAfterDelay; | |
target.addEventListener('click', this._showAfterDelay); | |
} | |
let hideOnMouseleaveListener = this._hideListenersOnTargetByEvent['mouseleave']; | |
if (hideOnMouseleaveListener) { | |
target.removeEventListener('mouseleave', hideOnMouseleaveListener); | |
delete this._hideListenersOnTargetByEvent['mouseleave']; | |
} | |
let hideOnBlurListener = this._hideListenersOnTargetByEvent['blur']; | |
if (hideOnBlurListener) { | |
target.removeEventListener('blur', hideOnBlurListener); | |
delete this._hideListenersOnTargetByEvent['blur']; | |
} | |
}, | |
}); |
This file contains hidden or 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'; | |
import layout from '../templates/components/ember-attacher'; | |
export default Ember.Component.extend({ | |
layout, | |
/** | |
* ================== PUBLIC CONFIG OPTIONS ================== | |
*/ | |
animation: 'fade', | |
arrow: false, | |
hideDelay: 0, | |
hideDuration: 400, | |
hideOn: 'mouseleave blur', | |
interactive: false, | |
placement: 'top', | |
popperClass: null, | |
popperOptions: null, | |
renderInPlace: false, | |
showDelay: 0, | |
showDuration: 400, | |
showOn: 'mouseenter focus', | |
target: Ember.computed(function() { | |
return this.element.parentNode; | |
}), | |
options: Ember.computed('animation', 'arrow', 'placement', 'popperOptions', function() { | |
let options = this.get('popperOptions') || {}; | |
// Deep copy the options | |
options = JSON.parse(JSON.stringify(options)) | |
let modifiers = options.modifiers || {}; | |
modifiers.arrow = modifiers.arrow || {}; | |
modifiers.arrow.enabled = this.get('arrow'); | |
options.modifiers = modifiers; | |
options.placement = this.get('placement'); | |
return options; | |
}), | |
}); |
This file contains hidden or 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({ | |
appName: 'Ember Twiddle' | |
}); |
This file contains hidden or 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
body { | |
height: 200vh; | |
} | |
.hover-me { | |
background-color: red; | |
width: 200px; | |
height: 100px; | |
} | |
.popper { | |
min-height: 10px; } | |
.popper > .inner { | |
max-width: 400px; | |
perspective: 800px; | |
z-index: 9999; } | |
.popper[x-placement=top] > .inner > div[x-arrow] { | |
transform: rotate(-45deg); | |
bottom: -5px; } | |
.popper[x-placement=top] > .inner.ember-attacher-none.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(-10px); } | |
.popper[x-placement=top] > .inner.ember-attacher-none.ember-attacher-hide { | |
opacity: 1; | |
transform: translateY(-10px); } | |
.popper[x-placement=top] > .inner.ember-attacher-perspective { | |
transform-origin: bottom; } | |
.popper[x-placement=top] > .inner.ember-attacher-perspective.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(-10px) rotateX(0); } | |
.popper[x-placement=top] > .inner.ember-attacher-perspective.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(0) rotateX(90deg); } | |
.popper[x-placement=top] > .inner.ember-attacher-fade.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(-10px); } | |
.popper[x-placement=top] > .inner.ember-attacher-fade.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(-10px); } | |
.popper[x-placement=top] > .inner.ember-attacher-shift.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(-10px); } | |
.popper[x-placement=top] > .inner.ember-attacher-shift.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(0); } | |
.popper[x-placement=top] > .inner.ember-attacher-scale.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(-10px) scale(1); } | |
.popper[x-placement=top] > .inner.ember-attacher-scale.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(0) scale(0); } | |
.popper[x-placement=bottom] > .inner > div[x-arrow] { | |
transform: rotate(135deg); | |
top: -5px; } | |
.popper[x-placement=bottom] > .inner.ember-attacher-none.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(10px); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-none.ember-attacher-hide { | |
opacity: 1; | |
transform: translateY(10px); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-perspective { | |
transform-origin: top; } | |
.popper[x-placement=bottom] > .inner.ember-attacher-perspective.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(10px) rotateX(0); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-perspective.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(0) rotateX(-90deg); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-fade.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(10px); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-fade.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(10px); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-shift.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(10px); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-shift.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(0); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-scale.ember-attacher-show { | |
opacity: 1; | |
transform: translateY(10px) scale(1); } | |
.popper[x-placement=bottom] > .inner.ember-attacher-scale.ember-attacher-hide { | |
opacity: 0; | |
transform: translateY(0) scale(0); } | |
.popper[x-placement=left] > .inner > div[x-arrow] { | |
transform: rotate(225deg); | |
right: -5px; | |
top: 50%; } | |
.popper[x-placement=left] > .inner.ember-attacher-none.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(-10px); } | |
.popper[x-placement=left] > .inner.ember-attacher-none.ember-attacher-hide { | |
opacity: 1; | |
transform: translateX(-10px); } | |
.popper[x-placement=left] > .inner.ember-attacher-perspective { | |
transform-origin: right; } | |
.popper[x-placement=left] > .inner.ember-attacher-perspective.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(-10px) rotateY(0); } | |
.popper[x-placement=left] > .inner.ember-attacher-perspective.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(0) rotateY(-90deg); } | |
.popper[x-placement=left] > .inner.ember-attacher-fade.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(-10px); } | |
.popper[x-placement=left] > .inner.ember-attacher-fade.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(-10px); } | |
.popper[x-placement=left] > .inner.ember-attacher-shift.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(-10px); } | |
.popper[x-placement=left] > .inner.ember-attacher-shift.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(0); } | |
.popper[x-placement=left] > .inner.ember-attacher-scale.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(-10px) scale(1); } | |
.popper[x-placement=left] > .inner.ember-attacher-scale.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(0) scale(0); } | |
.popper[x-placement=right] > .inner > div[x-arrow] { | |
transform: rotate(45deg); | |
left: -5px; | |
top: 50%; } | |
.popper[x-placement=right] > .inner.ember-attacher-none.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(10px); } | |
.popper[x-placement=right] > .inner.ember-attacher-none.ember-attacher-hide { | |
opacity: 1; | |
transform: translateX(10px); } | |
.popper[x-placement=right] > .inner.ember-attacher-perspective { | |
transform-origin: left; } | |
.popper[x-placement=right] > .inner.ember-attacher-perspective.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(10px) rotateY(0); } | |
.popper[x-placement=right] > .inner.ember-attacher-perspective.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(0) rotateY(90deg); } | |
.popper[x-placement=right] > .inner.ember-attacher-fade.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(10px); } | |
.popper[x-placement=right] > .inner.ember-attacher-fade.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(10px); } | |
.popper[x-placement=right] > .inner.ember-attacher-shift.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(10px); } | |
.popper[x-placement=right] > .inner.ember-attacher-shift.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(0); } | |
.popper[x-placement=right] > .inner.ember-attacher-scale.ember-attacher-show { | |
opacity: 1; | |
transform: translateX(10px) scale(1); } | |
.popper[x-placement=right] > .inner.ember-attacher-scale.ember-attacher-hide { | |
opacity: 0; | |
transform: translateX(0) scale(0); } | |
.tooltip > .inner, .popover > .inner { | |
position: relative; | |
color: white; | |
border-radius: 4px; | |
padding: 0.5rem 1rem; | |
text-align: center; | |
will-change: transform; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
background-color: #333; } | |
.tooltip > .inner > div[x-arrow], .popover > .inner > div[x-arrow] { | |
z-index: -1; | |
position: absolute; | |
width: 10px; | |
height: 10px; | |
background-color: #333; | |
border: 1px solid; | |
border-color: transparent transparent #333 #333; } | |
.popover > .inner { | |
color: black; | |
background-color: #fff; } | |
.popover > .inner > div[x-arrow] { | |
background-color: #fff; | |
border: none; } |
This file contains hidden or 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.12.1", | |
"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.12.0", | |
"ember-template-compiler": "2.12.0", | |
"ember-testing": "2.12.0" | |
}, | |
"addons": { | |
"ember-data": "2.12.1", | |
"ember-popper": "0.0.3" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment