Skip to content

Instantly share code, notes, and snippets.

@kybishop
Created May 2, 2017 23:19
Show Gist options
  • Save kybishop/eb65e0ec569d42990aa93fe557d4ac3c to your computer and use it in GitHub Desktop.
Save kybishop/eb65e0ec569d42990aa93fe557d4ac3c to your computer and use it in GitHub Desktop.
Scroll issues
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'];
}
},
});
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;
}),
});
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Ember Twiddle'
});
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; }
<div class="hover-me">
Hover to activate
{{#ember-attacher popperClass="popper tooltip"}}Hello{{/ember-attacher}}
</div>
{{yield}}
{{#if arrow}}
<div x-arrow></div>
{{/if}}
{{#ember-popper options=options
popperClass=popperClass
renderInPlace=renderInPlace
target=target as |emberPopper|}}
{{#ember-attacher-inner animation=animation
arrow=arrow
class="inner"
emberPopper=emberPopper
hideDelay=hideDelay
hideDuration=hideDuration
hideOn=hideOn
interactive=interactive
placement=placement
popper=emberPopper.popper
renderInPlace=renderInPlace
showDelay=showDelay
showDuration=showDuration
showOn=showOn
target=emberPopper.popperTarget}}
{{yield emberPopper}}
{{/ember-attacher-inner}}
{{/ember-popper}}
{
"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