From f0d23eb46cbe79b33874a97f91109199ec067f35 Mon Sep 17 00:00:00 2001 From: huumanoid Date: Tue, 28 Mar 2017 02:06:16 +0300 Subject: [PATCH 1/7] Add removal tracker prototype --- src/decorators/trackRemoval.js | 76 ++++++++++++++++++++++++++++++++++ src/index.js | 14 +++++-- 2 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 src/decorators/trackRemoval.js diff --git a/src/decorators/trackRemoval.js b/src/decorators/trackRemoval.js new file mode 100644 index 000000000..f9bc6783e --- /dev/null +++ b/src/decorators/trackRemoval.js @@ -0,0 +1,76 @@ +// http://stackoverflow.com/a/32726412/7571078 +const isDetached = (element) => { + if (element.parentNode === window.document) { + return false + } + if (element.parentNode == null) return true + return isDetached(element.parentNode) +} + +// https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/ +const getMutationObserverClass = () => { + return window.MutationObserver || + window.WebKitMutationObserver || + window.MozMutationObserver +} + +export default function (target) { + target.prototype.trackRemoval = function () { + if (this.removalObserver) { + this.releaseRemovalTracker() + } + + this.tracked = [] + + const MutationObserver = getMutationObserverClass() + if (MutationObserver) { + const observer = this.removalObserver = new MutationObserver(() => { + for (const element of this.tracked) { + if (isDetached(element) && element === this.state.currentTarget) { + this.hideTooltip() + } + } + }) + observer.observe(window.document, { childList: true, subtree: true }) + } + } + + target.prototype.attachRemovalTracker = function (element) { + this.tracked.push(element) + + const isMutationObserverAvailable = getMutationObserverClass() + if (!isMutationObserverAvailable) { + this.listeners = this.listeners || [] + + let listener = function (e) { + if (e.currentTarget === this.state.currentTarget) { + this.hideTooltip() + const idx = this.listeners.indexOf(listener) + this.listeners.splice(idx, 1) + } + } + listener = listener.bind(this) + + this.listeners.push({ + element, + listener + }) + + element.addEventListener('DOMNodeRemovedFromDocument', listener) + } + } + + target.prototype.releaseRemovalTracker = function () { + if (this.removalObserver) { + this.removalObserver.disconnect() + this.removalObserver = null + this.tracked = [] + } + if (this.listeners) { + for (const {listener, element} of this.listeners) { + element.removeEventListener('DOMNodeRemovedFromDocument', listener) + } + this.listeners = [] + } + } +} diff --git a/src/index.js b/src/index.js index 69e76c9cb..78cc328a5 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import windowListener from './decorators/windowListener' import customEvent from './decorators/customEvent' import isCapture from './decorators/isCapture' import getEffect from './decorators/getEffect' +import trackRemoval from './decorators/trackRemoval' /* Utils */ import getPosition from './utils/getPosition' @@ -20,7 +21,12 @@ import nodeListToArray from './utils/nodeListToArray' /* CSS */ import cssStyle from './style' -@staticMethods @windowListener @customEvent @isCapture @getEffect +@staticMethods +@windowListener +@customEvent +@isCapture +@getEffect +@trackRemoval class ReactTooltip extends Component { static propTypes = { @@ -163,6 +169,8 @@ class ReactTooltip extends Component { const {id, globalEventOff} = this.props let targetArray = this.getTargetArray(id) + this.trackRemoval() + targetArray.forEach(target => { const isCaptureMode = this.isCapture(target) const effect = this.getEffect(target) @@ -181,7 +189,7 @@ class ReactTooltip extends Component { target.addEventListener('mousemove', this.updateTooltip, isCaptureMode) } target.addEventListener('mouseleave', this.hideTooltip, isCaptureMode) - target.addEventListener('DOMNodeRemovedFromDocument', this.checkSameTarget, isCaptureMode) + this.attachRemovalTracker(target) }) // Global event to hide tooltip @@ -203,6 +211,7 @@ class ReactTooltip extends Component { }) if (globalEventOff) window.removeEventListener(globalEventOff, this.hideTooltip) + this.releaseRemovalTracker() } /** @@ -215,7 +224,6 @@ class ReactTooltip extends Component { target.removeEventListener('mouseenter', this.showTooltip, isCaptureMode) target.removeEventListener('mousemove', this.updateTooltip, isCaptureMode) target.removeEventListener('mouseleave', this.hideTooltip, isCaptureMode) - target.removeEventListener('DOMNodeRemovedFromDocument', this.checkSameTarget, isCaptureMode) } /** From 720f2b8a6d9e1d80fdfa909f8051deb2ef64af4c Mon Sep 17 00:00:00 2001 From: huumanoid Date: Tue, 28 Mar 2017 02:35:21 +0300 Subject: [PATCH 2/7] Refactor removal tracker --- src/decorators/trackRemoval.js | 140 +++++++++++++++++++++------------ src/index.js | 4 +- 2 files changed, 92 insertions(+), 52 deletions(-) diff --git a/src/decorators/trackRemoval.js b/src/decorators/trackRemoval.js index f9bc6783e..897445df8 100644 --- a/src/decorators/trackRemoval.js +++ b/src/decorators/trackRemoval.js @@ -1,12 +1,3 @@ -// http://stackoverflow.com/a/32726412/7571078 -const isDetached = (element) => { - if (element.parentNode === window.document) { - return false - } - if (element.parentNode == null) return true - return isDetached(element.parentNode) -} - // https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/ const getMutationObserverClass = () => { return window.MutationObserver || @@ -14,63 +5,112 @@ const getMutationObserverClass = () => { window.MozMutationObserver } -export default function (target) { - target.prototype.trackRemoval = function () { - if (this.removalObserver) { - this.releaseRemovalTracker() +const isMutationObserverAvailable = () => { + return getMutationObserverClass() != null +} + +class EventBasedRemovalTracker { + constructor (tooltip) { + this.tooltip = tooltip + this.listeners = [] + } + + attach (element) { + const {tooltip} = this + + let listener = function (e) { + if (e.currentTarget === tooltip.state.currentTarget) { + tooltip.hideTooltip() + this.listeners.splice(this.listeners.indexOf(listener), 1) + } } + listener = listener.bind(this) + + this.listeners.push({ + element, + listener + }) - this.tracked = [] + element.addEventListener('DOMNodeRemovedFromDocument', listener) + } + + unbind () { + for (const {listener, element} of this.listeners) { + element.removeEventListener('DOMNodeRemovedFromDocument', listener) + } + this.listeners = [] + } +} + +class MutationBasedRemovalTracker { + constructor (tooltip) { + this.tooltip = tooltip + + this.observer = null + this.inited = false + this.trackedElements = null + } + + init () { + if (this.inited) { + this.unbind() + } + + this.trackedElements = [] const MutationObserver = getMutationObserverClass() if (MutationObserver) { - const observer = this.removalObserver = new MutationObserver(() => { - for (const element of this.tracked) { - if (isDetached(element) && element === this.state.currentTarget) { - this.hideTooltip() + this.observer = new MutationObserver(() => { + for (const element of this.trackedElements) { + if (this.isDetached(element) && element === this.tooltip.state.currentTarget) { + this.tooltip.hideTooltip() } } }) - observer.observe(window.document, { childList: true, subtree: true }) + this.observer.observe(window.document, { childList: true, subtree: true }) } - } - target.prototype.attachRemovalTracker = function (element) { - this.tracked.push(element) - - const isMutationObserverAvailable = getMutationObserverClass() - if (!isMutationObserverAvailable) { - this.listeners = this.listeners || [] + this.inited = true + } - let listener = function (e) { - if (e.currentTarget === this.state.currentTarget) { - this.hideTooltip() - const idx = this.listeners.indexOf(listener) - this.listeners.splice(idx, 1) - } - } - listener = listener.bind(this) + unbind () { + if (this.observer) { + this.observer.disconnect() + this.observer = null + this.trackedElements = null + } + this.inited = false + } - this.listeners.push({ - element, - listener - }) + attach (element) { + this.trackedElements.push(element) + } - element.addEventListener('DOMNodeRemovedFromDocument', listener) + // http://stackoverflow.com/a/32726412/7571078 + isDetached (element) { + if (element.parentNode === window.document) { + return false } + if (element.parentNode == null) return true + return this.isDetached(element.parentNode) } +} - target.prototype.releaseRemovalTracker = function () { - if (this.removalObserver) { - this.removalObserver.disconnect() - this.removalObserver = null - this.tracked = [] - } - if (this.listeners) { - for (const {listener, element} of this.listeners) { - element.removeEventListener('DOMNodeRemovedFromDocument', listener) - } - this.listeners = [] +export default function (target) { + target.prototype.bindRemovalTracker = function () { + if (isMutationObserverAvailable()) { + this.removalTracker = new MutationBasedRemovalTracker(this) + this.removalTracker.init() + } else { + this.removalTracker = new EventBasedRemovalTracker(this) } } + + target.prototype.attachRemovalTracker = function (element) { + this.removalTracker.attach(element) + } + + target.prototype.unbindRemovalTracker = function () { + this.removalTracker.unbind() + } } diff --git a/src/index.js b/src/index.js index 78cc328a5..cf968ac25 100644 --- a/src/index.js +++ b/src/index.js @@ -169,7 +169,7 @@ class ReactTooltip extends Component { const {id, globalEventOff} = this.props let targetArray = this.getTargetArray(id) - this.trackRemoval() + this.bindRemovalTracker() targetArray.forEach(target => { const isCaptureMode = this.isCapture(target) @@ -211,7 +211,7 @@ class ReactTooltip extends Component { }) if (globalEventOff) window.removeEventListener(globalEventOff, this.hideTooltip) - this.releaseRemovalTracker() + this.unbindRemovalTracker() } /** From 54bed6a470e5f1066a9c133dd03ce782c67562bf Mon Sep 17 00:00:00 2001 From: huumanoid Date: Tue, 28 Mar 2017 02:53:41 +0300 Subject: [PATCH 3/7] Add cleanup --- src/decorators/trackRemoval.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decorators/trackRemoval.js b/src/decorators/trackRemoval.js index 897445df8..019b8337d 100644 --- a/src/decorators/trackRemoval.js +++ b/src/decorators/trackRemoval.js @@ -112,5 +112,6 @@ export default function (target) { target.prototype.unbindRemovalTracker = function () { this.removalTracker.unbind() + this.removalTracker = null } } From d927d0e1c8bd97a81f565dee746e1b1d121bef56 Mon Sep 17 00:00:00 2001 From: huumanoid Date: Tue, 28 Mar 2017 22:58:47 +0300 Subject: [PATCH 4/7] Refactor EventBasedRemovalTracker --- src/decorators/trackRemoval.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/decorators/trackRemoval.js b/src/decorators/trackRemoval.js index 019b8337d..34faf4526 100644 --- a/src/decorators/trackRemoval.js +++ b/src/decorators/trackRemoval.js @@ -18,13 +18,12 @@ class EventBasedRemovalTracker { attach (element) { const {tooltip} = this - let listener = function (e) { + const listener = (e) => { if (e.currentTarget === tooltip.state.currentTarget) { tooltip.hideTooltip() this.listeners.splice(this.listeners.indexOf(listener), 1) } } - listener = listener.bind(this) this.listeners.push({ element, From 3cc17b0b01d925f9b997cedf1430f179d43054ee Mon Sep 17 00:00:00 2001 From: huumanoid Date: Tue, 28 Mar 2017 22:59:12 +0300 Subject: [PATCH 5/7] Simplify MutationBasedRemovalTracker Stateless! It #137-ready seamlessly. --- src/decorators/trackRemoval.js | 42 +++++++++++++--------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/decorators/trackRemoval.js b/src/decorators/trackRemoval.js index 34faf4526..2a80a7bf4 100644 --- a/src/decorators/trackRemoval.js +++ b/src/decorators/trackRemoval.js @@ -47,52 +47,40 @@ class MutationBasedRemovalTracker { this.observer = null this.inited = false - this.trackedElements = null } init () { if (this.inited) { this.unbind() } - - this.trackedElements = [] + this.inited = true const MutationObserver = getMutationObserverClass() - if (MutationObserver) { - this.observer = new MutationObserver(() => { - for (const element of this.trackedElements) { - if (this.isDetached(element) && element === this.tooltip.state.currentTarget) { + if (!MutationObserver) { + return + } + + this.observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const element of mutation.removedNodes) { + if (element === this.tooltip.state.currentTarget) { this.tooltip.hideTooltip() + return } } - }) - this.observer.observe(window.document, { childList: true, subtree: true }) - } + } + }) - this.inited = true + this.observer.observe(window.document, { childList: true, subtree: true }) } unbind () { if (this.observer) { this.observer.disconnect() this.observer = null - this.trackedElements = null } this.inited = false } - - attach (element) { - this.trackedElements.push(element) - } - - // http://stackoverflow.com/a/32726412/7571078 - isDetached (element) { - if (element.parentNode === window.document) { - return false - } - if (element.parentNode == null) return true - return this.isDetached(element.parentNode) - } } export default function (target) { @@ -106,7 +94,9 @@ export default function (target) { } target.prototype.attachRemovalTracker = function (element) { - this.removalTracker.attach(element) + if (this.removalTracker.attach) { + this.removalTracker.attach(element) + } } target.prototype.unbindRemovalTracker = function () { From c721288cbf0adb03f0c69973364edc5a54b662ed Mon Sep 17 00:00:00 2001 From: huumanoid Date: Tue, 28 Mar 2017 23:14:00 +0300 Subject: [PATCH 6/7] Get rid of EventBasedRemovalTracker It seems like it's a bad idea to support MutationEvent-based removal tracker. https://developer.mozilla.org/en-US/docs/Web/API/MutationEvent According to the document above, there are a lot of issues with MutationEvents, and they are even not broadly supported in browsers. Mutation observer polyfill: https://github.com/Polymer/MutationObservers --- src/decorators/trackRemoval.js | 44 ++-------------------------------- src/index.js | 1 - 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/src/decorators/trackRemoval.js b/src/decorators/trackRemoval.js index 2a80a7bf4..7a5cf89f5 100644 --- a/src/decorators/trackRemoval.js +++ b/src/decorators/trackRemoval.js @@ -9,39 +9,7 @@ const isMutationObserverAvailable = () => { return getMutationObserverClass() != null } -class EventBasedRemovalTracker { - constructor (tooltip) { - this.tooltip = tooltip - this.listeners = [] - } - - attach (element) { - const {tooltip} = this - - const listener = (e) => { - if (e.currentTarget === tooltip.state.currentTarget) { - tooltip.hideTooltip() - this.listeners.splice(this.listeners.indexOf(listener), 1) - } - } - - this.listeners.push({ - element, - listener - }) - - element.addEventListener('DOMNodeRemovedFromDocument', listener) - } - - unbind () { - for (const {listener, element} of this.listeners) { - element.removeEventListener('DOMNodeRemovedFromDocument', listener) - } - this.listeners = [] - } -} - -class MutationBasedRemovalTracker { +class ObserverBasedRemovalTracker { constructor (tooltip) { this.tooltip = tooltip @@ -86,16 +54,8 @@ class MutationBasedRemovalTracker { export default function (target) { target.prototype.bindRemovalTracker = function () { if (isMutationObserverAvailable()) { - this.removalTracker = new MutationBasedRemovalTracker(this) + this.removalTracker = new ObserverBasedRemovalTracker(this) this.removalTracker.init() - } else { - this.removalTracker = new EventBasedRemovalTracker(this) - } - } - - target.prototype.attachRemovalTracker = function (element) { - if (this.removalTracker.attach) { - this.removalTracker.attach(element) } } diff --git a/src/index.js b/src/index.js index cf968ac25..2dd0bb95d 100644 --- a/src/index.js +++ b/src/index.js @@ -189,7 +189,6 @@ class ReactTooltip extends Component { target.addEventListener('mousemove', this.updateTooltip, isCaptureMode) } target.addEventListener('mouseleave', this.hideTooltip, isCaptureMode) - this.attachRemovalTracker(target) }) // Global event to hide tooltip From cf6427e1cfe310578fd8cb531af48a5d4012e6f0 Mon Sep 17 00:00:00 2001 From: huumanoid Date: Tue, 28 Mar 2017 23:26:48 +0300 Subject: [PATCH 7/7] Refactor removal tracker --- src/decorators/trackRemoval.js | 64 +++++++++++----------------------- src/index.js | 12 ++----- 2 files changed, 24 insertions(+), 52 deletions(-) diff --git a/src/decorators/trackRemoval.js b/src/decorators/trackRemoval.js index 7a5cf89f5..d072ea551 100644 --- a/src/decorators/trackRemoval.js +++ b/src/decorators/trackRemoval.js @@ -1,3 +1,12 @@ +/** + * Tracking target removing from DOM. + * It's nessesary to hide tooltip when it's target disappears. + * Otherwise, the tooltip would be shown forever until another target + * is triggered. + * + * If MutationObserver is not available, this feature just doesn't work. + */ + // https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/ const getMutationObserverClass = () => { return window.MutationObserver || @@ -5,62 +14,31 @@ const getMutationObserverClass = () => { window.MozMutationObserver } -const isMutationObserverAvailable = () => { - return getMutationObserverClass() != null -} - -class ObserverBasedRemovalTracker { - constructor (tooltip) { - this.tooltip = tooltip - - this.observer = null - this.inited = false - } - - init () { - if (this.inited) { - this.unbind() - } - this.inited = true - +export default function (target) { + target.prototype.bindRemovalTracker = function () { const MutationObserver = getMutationObserverClass() - if (!MutationObserver) { - return - } + if (MutationObserver == null) return - this.observer = new MutationObserver((mutations) => { + const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const element of mutation.removedNodes) { - if (element === this.tooltip.state.currentTarget) { - this.tooltip.hideTooltip() + if (element === this.state.currentTarget) { + this.hideTooltip() return } } } }) - this.observer.observe(window.document, { childList: true, subtree: true }) - } - - unbind () { - if (this.observer) { - this.observer.disconnect() - this.observer = null - } - this.inited = false - } -} + observer.observe(window.document, { childList: true, subtree: true }) -export default function (target) { - target.prototype.bindRemovalTracker = function () { - if (isMutationObserverAvailable()) { - this.removalTracker = new ObserverBasedRemovalTracker(this) - this.removalTracker.init() - } + this.removalTracker = observer } target.prototype.unbindRemovalTracker = function () { - this.removalTracker.unbind() - this.removalTracker = null + if (this.removalTracker) { + this.removalTracker.disconnect() + this.removalTracker = null + } } } diff --git a/src/index.js b/src/index.js index 2dd0bb95d..24780c721 100644 --- a/src/index.js +++ b/src/index.js @@ -92,7 +92,6 @@ class ReactTooltip extends Component { this.bind([ 'showTooltip', 'updateTooltip', - 'checkSameTarget', 'hideTooltip', 'globalRebuild', 'globalShow', @@ -169,8 +168,6 @@ class ReactTooltip extends Component { const {id, globalEventOff} = this.props let targetArray = this.getTargetArray(id) - this.bindRemovalTracker() - targetArray.forEach(target => { const isCaptureMode = this.isCapture(target) const effect = this.getEffect(target) @@ -196,6 +193,9 @@ class ReactTooltip extends Component { window.removeEventListener(globalEventOff, this.hideTooltip) window.addEventListener(globalEventOff, this.hideTooltip, false) } + + // Track removal of targetArray elements from DOM + this.bindRemovalTracker() } /** @@ -339,12 +339,6 @@ class ReactTooltip extends Component { } } - checkSameTarget (e) { - if (this.state.currentTarget === e.currentTarget) { - this.hideTooltip(e) - } - } - /** * When mouse leave, hide tooltip */