-
Notifications
You must be signed in to change notification settings - Fork 141
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #104 from a15n/a15n/lazy-render
A15n/lazy render
- Loading branch information
Showing
35 changed files
with
841 additions
and
278 deletions.
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import Ember from 'ember'; | ||
import layout from 'ember-tooltips/templates/components/lazy-render-wrapper'; | ||
|
||
const { computed, get, run, $ } = Ember; | ||
|
||
// https://github.com/emberjs/rfcs/issues/168 | ||
// https://github.com/emberjs/ember.js/pull/12500 | ||
function getParent(view) { | ||
if (get(view, 'tagName') === '') { | ||
// Beware: use of private API! :( | ||
if (Ember.ViewUtils && Ember.ViewUtils.getViewBounds) { | ||
return $(Ember.ViewUtils.getViewBounds(view).parentElement); | ||
} else { | ||
return $(view._renderNode.contextualElement); | ||
} | ||
} else { | ||
return view.$().parent(); | ||
} | ||
} | ||
|
||
// this const is also used in lazy-render-test.js | ||
// to ensure each interaction type causes a render | ||
export const INTERACTION_EVENT_TYPES = ['mouseenter', 'click', 'focusin']; | ||
|
||
const PASSABLE_PROPERTIES = [ | ||
'delay', | ||
'delayOnChange', | ||
'duration', | ||
'effect', | ||
'event', | ||
'hideOn', | ||
'keepInWindow', | ||
'side', | ||
'showOn', | ||
'spacing', | ||
'isShown', | ||
'tooltipIsVisible', | ||
'hideDelay', | ||
'target', | ||
|
||
// non-publicized attributes | ||
'updateFor', | ||
'targetAttachment', | ||
'attachment', | ||
'role', | ||
'tabindex', | ||
]; | ||
|
||
const PASSABLE_ACTIONS = [ | ||
'onDestroy', | ||
'onHide', | ||
'onRender', | ||
'onShow', | ||
|
||
// deprecated lifecycle actions | ||
'onTooltipDestroy', | ||
'onTooltipHide', | ||
'onTooltipRender', | ||
'onTooltipShow', | ||
]; | ||
|
||
const PASSABLE_OPTIONS = PASSABLE_PROPERTIES.concat(PASSABLE_ACTIONS); | ||
|
||
export default Ember.Component.extend({ | ||
tagName: '', | ||
layout, | ||
|
||
passedPropertiesObject: computed(...PASSABLE_OPTIONS, function() { | ||
return PASSABLE_OPTIONS.reduce((passablePropertiesObject, key) => { | ||
// if a property has been declared by Component extension ( TooltipOnElement.extend ) | ||
// or by handlebars instantiation ( {{tooltip-on-element}} ) then that property needs | ||
// to be passed from this wrapper to the lazy-rendered tooltip or popover component | ||
|
||
let value = this.get(key); | ||
|
||
if (!Ember.isNone(value)) { | ||
if (PASSABLE_ACTIONS.indexOf(key) >= 0) { | ||
// if a user has declared a lifecycle action property (onShow='someFunc') | ||
// then we must pass down the correctly-scoped action instead of value | ||
|
||
passablePropertiesObject[key] = () => this.sendAction(key); | ||
} else { | ||
passablePropertiesObject[key] = value; | ||
} | ||
} | ||
|
||
return passablePropertiesObject; | ||
}, {}); | ||
}), | ||
|
||
enableLazyRendering: false, | ||
_hasUserInteracted: false, | ||
_hasRendered: false, | ||
_shouldRender: computed('isShown', 'tooltipIsVisible', 'enableLazyRendering', '_hasUserInteracted', function() { | ||
// if isShown, tooltipIsVisible, !enableLazyRendering, or _hasUserInteracted then | ||
// we return true and set _hasRendered to true because | ||
// there is never a scenario where this wrapper should destroy the tooltip | ||
|
||
if (this.get('_hasRendered')) { | ||
|
||
return true; | ||
|
||
} else if (this.get('isShown') || this.get('tooltipIsVisible')) { | ||
|
||
this.set('_hasRendered', true); | ||
return true; | ||
|
||
} else if (!this.get('enableLazyRendering')) { | ||
|
||
this.set('_hasRendered', true); | ||
return true; | ||
|
||
} else if (this.get('_hasUserInteracted')) { | ||
|
||
this.set('_hasRendered', true); | ||
return true; | ||
|
||
} | ||
|
||
return false; | ||
}), | ||
|
||
didInsertElement() { | ||
this._super(...arguments); | ||
|
||
if (this.get('_shouldRender')) { | ||
// if the tooltip _shouldRender then we don't need | ||
// any special $parent event handling | ||
return; | ||
} | ||
|
||
const $parent = getParent(this); | ||
|
||
INTERACTION_EVENT_TYPES.forEach((eventType) => { | ||
$parent.on(`${eventType}.lazy-ember-popover`, () => { | ||
if (this.get('_hasUserInteracted')) { | ||
$parent.off(`${eventType}.lazy-ember-popover`); | ||
} else { | ||
this.set('_hasUserInteracted', true); | ||
run.next(() => { | ||
$parent.trigger(eventType); | ||
}); | ||
} | ||
}); | ||
}); | ||
}, | ||
|
||
willDestroyElement() { | ||
this._super(...arguments); | ||
|
||
const $parent = getParent(this); | ||
INTERACTION_EVENT_TYPES.forEach((eventType) => { | ||
$parent.off(`${eventType}.lazy-ember-popover`); | ||
}); | ||
}, | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,5 @@ | ||
import PopoverOnElementComponent from 'ember-tooltips/components/popover-on-element'; | ||
import { onComponentTarget } from 'ember-tooltips/utils'; | ||
|
||
export default PopoverOnElementComponent.extend({ | ||
|
||
target: onComponentTarget, | ||
|
||
tetherComponentName: 'tether-popover-on-component', | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,122 +1,14 @@ | ||
import Ember from 'ember'; | ||
import TooltipAndPopoverComponent from 'ember-tooltips/components/tooltip-and-popover'; | ||
import layout from 'ember-tooltips/templates/components/popover-on-element'; | ||
import LazyRenderWrapperComponent from 'ember-tooltips/components/lazy-render-wrapper'; | ||
|
||
const { $, run } = Ember; | ||
export default LazyRenderWrapperComponent.extend({ | ||
|
||
export default TooltipAndPopoverComponent.extend({ | ||
tetherComponentName: 'tether-popover-on-element', | ||
|
||
/* Options */ | ||
hideDelay: 250, | ||
|
||
/* Properties */ | ||
layout, | ||
classNames: ['ember-popover'], | ||
_isMouseInside: false, | ||
didInsertElement() { | ||
this._super(...arguments); | ||
|
||
const event = this.get('event'); | ||
const target = this.get('target'); | ||
const $target = $(target); | ||
const $popover = this.$(); | ||
|
||
if (event === 'none') { | ||
|
||
return; | ||
|
||
} else if (event === 'hover') { | ||
|
||
const _showOn = this.get('_showOn'); | ||
const _hideOn = this.get('_hideOn'); | ||
|
||
// _showOn == 'mouseenter' | ||
$target.on(_showOn, () => this.show()); | ||
|
||
// _hideOn == 'mouseleave' | ||
$target.add($popover).on(_hideOn, () => { | ||
run.later(() => { | ||
if (!this.get('_isMouseInside')) { | ||
this.hide(); | ||
} | ||
}, +this.get('hideDelay')); | ||
}); | ||
|
||
// we must use mouseover/mouseout because they correctly | ||
// register hover interactivity when spacing='0' | ||
$target.add($popover).on('mouseover', () => this.set('_isMouseInside', true)); | ||
$target.add($popover).on('mouseout', () => this.set('_isMouseInside', false)); | ||
|
||
} else if (event === 'click') { | ||
|
||
$(document).on(`click.${target}`, (event) => { | ||
// this lightweight, name-spaced click handler is necessary to determine | ||
// if a click is NOT on $target and NOT an ancestor of $target. | ||
// If so then it must be a click elsewhere and should close the popover | ||
// see... https://css-tricks.com/dangers-stopping-event-propagation/ | ||
const isClickedElementElsewhere = this._isElementElsewhere(event.target); | ||
const isPopoverShown = this.get('isShown'); | ||
|
||
if (isClickedElementElsewhere && isPopoverShown) { | ||
this.hide(); | ||
} | ||
}); | ||
|
||
// we use mousedown because it occurs before the focus event | ||
$target.on('mousedown', (event) => { | ||
// $target.on('mousedown') is called when the $popover is | ||
// clicked because the $popover is contained within the $target. | ||
// This will ignores those types of clicks. | ||
const isMouseDownElementInPopover = this._isElementInPopover(event.target); | ||
if (isMouseDownElementInPopover) { | ||
return; | ||
} | ||
this.toggle(); | ||
}); | ||
} | ||
|
||
$target.on('focus', () => this.show()); | ||
|
||
$popover.on('focusout', () => { | ||
// use a run.later() to allow the 'focusout' event to finish handling | ||
run.later(() => { | ||
const isFocusedElementElsewhere = this._isElementElsewhere(document.activeElement); | ||
if (isFocusedElementElsewhere) { | ||
this.hide(); | ||
} | ||
}); | ||
}); | ||
}, | ||
willDestroyElement() { | ||
this._super(...arguments); | ||
|
||
const target = this.get('target'); | ||
const $target = $(target); | ||
const $popover = this.$(); | ||
const _showOn = this.get('_showOn'); | ||
const _hideOn = this.get('_hideOn'); | ||
|
||
$target.add($popover).off(`${_showOn} mouseover ${_hideOn} mouseout mousedown focus focusout`); | ||
|
||
$(document).off(`click.${target}`); | ||
}, | ||
_isElementInPopover(newElement) { | ||
// determines if newElement is $popover or contained within $popover | ||
const $popover = this.$(); | ||
return $popover.is(newElement) || $popover.find(newElement).length; | ||
}, | ||
_isElementElsewhere(newElement) { | ||
// determines if newElement is not $target, not $popover, and not contained within either | ||
const $target = $(this.get('target')); | ||
|
||
const isNewElementOutsideTarget = !$target.is(newElement) && !$target.find(newElement).length; | ||
const isNewElementOutsidePopover = !this._isElementInPopover(newElement); | ||
|
||
return isNewElementOutsideTarget && isNewElementOutsidePopover; | ||
}, | ||
actions: { | ||
hide() { | ||
this.hide(); | ||
} | ||
}, | ||
childView: null, // this is set during the childView's didRender and is needed for the hide action | ||
actions: { | ||
hide() { | ||
const childView = this.get('childView'); | ||
childView.send('hide'); | ||
}, | ||
}, | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import Ember from 'ember'; | ||
|
||
// empty component used for `-on-component` tests | ||
export default Ember.Component.extend({ | ||
classNames: 'some-component', | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import TetherPopoverOnElement from 'ember-tooltips/components/tether-popover-on-element'; | ||
import { onComponentTarget } from 'ember-tooltips/utils'; | ||
|
||
export default TetherPopoverOnElement.extend({ | ||
|
||
target: onComponentTarget, | ||
|
||
}); |
Oops, something went wrong.