Skip to content

Commit

Permalink
fix(tooltips, popovers): fix memory leak (closes #4400) (#4401)
Browse files Browse the repository at this point in the history
* fix(v-b-tooltip): memory leaks

* Update bv-tooltip.js

* Update bv-tooltip.js

* Update bv-tooltip.js

* Update bv-tooltip.js

* Update bv-tooltip.js

* Update bv-tooltip.js
  • Loading branch information
jacobmllr95 committed Nov 20, 2019
1 parent 0518691 commit c71352d
Showing 1 changed file with 52 additions and 74 deletions.
126 changes: 52 additions & 74 deletions src/components/tooltip/helpers/bv-tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import Vue from '../../../utils/vue'
import getScopId from '../../../utils/get-scope-id'
import looseEqual from '../../../utils/loose-equal'
import noop from '../../../utils/noop'
import { arrayIncludes, concat, from as arrayFrom } from '../../../utils/array'
import {
isElement,
Expand Down Expand Up @@ -34,7 +35,6 @@ import {
import { keys } from '../../../utils/object'
import { warn } from '../../../utils/warn'
import { BvEvent } from '../../../utils/bv-event.class'

import { BVTooltipTemplate } from './bv-tooltip-template'

const NAME = 'BVTooltip'
Expand Down Expand Up @@ -203,7 +203,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
this.$_hoverState = ''
this.$_visibleInterval = null
this.$_enabled = !this.disabled
this.$_noop = () => {}
this.$_noop = noop.bind(this)

// Destroy ourselves when the parent is destroyed
if (this.$parent) {
Expand Down Expand Up @@ -236,18 +236,14 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
// Remove all handler/listeners
this.unListen()
this.setWhileOpenListeners(false)

// Clear any timeouts/Timers
clearTimeout(this.$_hoverTimeout)
this.$_hoverTimeout = null

// Clear any timeouts/intervals
this.clearHoverTimeout()
this.clearVisibilityInterval()
// Destroy the template
this.destroyTemplate()
this.restoreTitle()
},
methods: {
//
// Methods for creating and destroying the template
//
// --- Methods for creating and destroying the template ---
getTemplate() {
// Overridden by BVPopover
return BVTooltipTemplate
Expand All @@ -273,7 +269,6 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
},
createTemplateAndShow() {
// Creates the template instance and show it
// this.destroyTemplate()
const container = this.getContainer()
const Template = this.getTemplate()
const $tip = (this.$_tip = new Template({
Expand Down Expand Up @@ -323,19 +318,24 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
// then emit the `hidden` event once it is fully hidden
// The `hook:destroyed` will also be called (safety measure)
this.$_tip && this.$_tip.hide()
// Clear out any stragging active triggers
this.clearActiveTriggers()
// Reset the hover state
this.$_hoverState = ''
},
// Destroy the template instance and reset state
destroyTemplate() {
// Destroy the template instance and reset state
this.setWhileOpenListeners(false)
clearTimeout(this.$_hoverTimeout)
this.$_hoverTimout = null
this.clearHoverTimeout()
this.$_hoverState = ''
this.clearActiveTriggers()
this.localPlacementTarget = null
try {
this.$_tip && this.$_tip.$destroy()
} catch {}
this.$_tip = null
this.removeAriaDescribedby()
this.restoreTitle()
this.localShow = false
},
getTemplateElement() {
Expand All @@ -355,13 +355,10 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
})
}
},
//
// Show and Hide handlers
//
// --- Show/Hide handlers ---
// Show the tooltip
show() {
// Show the tooltip
const target = this.getTarget()

if (
!target ||
!contains(document.body, target) ||
Expand All @@ -375,38 +372,29 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
// we exit without showing
return
}

// If tip already exists, exit early
if (this.$_tip || this.localShow) {
// If tip already exists, exit early
/* istanbul ignore next */
return
}

// In the process of showing
this.localShow = true

// Create a cancelable BvEvent
const showEvt = this.buildEvent('show', { cancelable: true })
this.emitEvent(showEvt)
// Don't show if event cancelled
/* istanbul ignore next: ignore for now */
if (showEvt.defaultPrevented) {
// Don't show if event cancelled
// Destroy the template (if for some reason it was created)
/* istanbul ignore next */
this.destroyTemplate()
// Clear the localShow flag
/* istanbul ignore next */
this.localShow = false
/* istanbul ignore next */
return
}

// Fix the title attribute on target
this.fixTitle()

// Set aria-describedby on target
this.addAriaDescribedby()

// Create and show the tooltip
this.createTemplateAndShow()
},
Expand All @@ -433,11 +421,6 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({

// Tell the template to hide
this.hideTemplate()
// TODO: The following could be added to `hideTemplate()`
// Clear out any stragging active triggers
this.clearActiveTriggers()
// Reset the hover state
this.$_hoverState = ''
},
forceHide() {
// Forcefully hides/destroys the template, regardless of any active triggers
Expand All @@ -450,8 +433,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
// This is also done in the template `hide` evt handler
this.setWhileOpenListeners(false)
// Clear any hover enter/leave event
clearTimeout(this.hoverTimeout)
this.$_hoverTimeout = null
this.clearHoverTimeout()
this.$_hoverState = ''
this.clearActiveTriggers()
// Disable the fade animation on the template
Expand All @@ -464,49 +446,42 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
enable() {
this.$_enabled = true
// Create a non-cancelable BvEvent
this.emitEvent(this.buildEvent('enabled', {}))
this.emitEvent(this.buildEvent('enabled'))
},
disable() {
this.$_enabled = false
// Create a non-cancelable BvEvent
this.emitEvent(this.buildEvent('disabled', {}))
this.emitEvent(this.buildEvent('disabled'))
},
//
// Handlers for template events
//
// --- Handlers for template events ---
// When template is inserted into DOM, but not yet shown
onTemplateShow() {
// When template is inserted into DOM, but not yet shown
// Enable while open listeners/watchers
this.setWhileOpenListeners(true)
},
// When template show transition completes
onTemplateShown() {
// When template show transition completes
const prevHoverState = this.$_hoverState
this.$_hoverState = ''
if (prevHoverState === 'out') {
this.leave(null)
}
// Emit a non-cancelable BvEvent 'shown'
this.emitEvent(this.buildEvent('shown', {}))
this.emitEvent(this.buildEvent('shown'))
},
// When template is starting to hide
onTemplateHide() {
// When template is starting to hide
// Disable while open listeners/watchers
this.setWhileOpenListeners(false)
},
// When template has completed closing (just before it self destructs)
onTemplateHidden() {
// When template has completed closing (just before it self destructs)
// TODO:
// The next two lines could be moved into `destroyTemplate()`
this.removeAriaDescribedby()
this.restoreTitle()
// Destroy the template
this.destroyTemplate()
// Emit a non-cancelable BvEvent 'shown'
this.emitEvent(this.buildEvent('hidden', {}))
},
//
// Utility methods
//
// --- Utility methods ---
getTarget() {
// Handle case where target may be a component ref
let target = this.target ? this.target.$el || this.target : null
Expand Down Expand Up @@ -566,6 +541,18 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
const target = this.getTarget()
return this.isDropdown() && target && select(DROPDOWN_OPEN_SELECTOR, target)
},
clearHoverTimeout() {
if (this.$_hoverTimeout) {
clearTimeout(this.$_hoverTimeout)
this.$_hoverTimeout = null
}
},
clearVisibilityInterval() {
if (this.$_visibleInterval) {
clearInterval(this.$_visibleInterval)
this.$_visibleInterval = null
}
},
clearActiveTriggers() {
for (const trigger in this.activeTrigger) {
this.activeTrigger[trigger] = false
Expand Down Expand Up @@ -619,9 +606,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
removeAttr(target, 'data-original-title')
}
},
//
// BvEvent helpers
//
// --- BvEvent helpers ---
buildEvent(type, opts = {}) {
// Defaults to a non-cancellable event
return new BvEvent(type, {
Expand All @@ -644,20 +629,16 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
}
this.$emit(evtName, bvEvt)
},
//
// Event handler setup methods
//
// --- Event handler setup methods ---
listen() {
// Enable trigger event handlers
const el = this.getTarget()
if (!el) {
/* istanbul ignore next */
return
}

// Listen for global show/hide events
this.setRootListener(true)

// Set up our listeners on the target trigger element
this.computedTriggers.forEach(trigger => {
if (trigger === 'click') {
Expand Down Expand Up @@ -712,14 +693,13 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
// On-touch start listeners
this.setOnTouchStartListener(on)
},
// Handler for periodic visibility check
visibleCheck(on) {
// Handler for periodic visibility check
clearInterval(this.$_visibleInterval)
this.$_visibleInterval = null
this.clearVisibilityInterval()
const target = this.getTarget()
const tip = this.getTemplateElement()
if (on) {
this.visibleInterval = setInterval(() => {
this.$_visibleInterval = setInterval(() => {
if (tip && this.localShow && (!target.parentNode || !isVisible(target))) {
// Target element is no longer visible or not in DOM, so force-hide the tooltip
this.forceHide()
Expand Down Expand Up @@ -762,9 +742,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
target.__vue__[on ? '$on' : '$off']('shown', this.forceHide)
}
},
//
// Event handlers
//
// --- Event handlers ---
handleEvent(evt) {
// General trigger event handler
// target is the trigger element
Expand Down Expand Up @@ -884,14 +862,14 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
this.$_hoverState = 'in'
return
}
clearTimeout(this.hoverTimeout)
this.clearHoverTimeout()
this.$_hoverState = 'in'
if (!this.computedDelay.show) {
this.show()
} else {
// Hide any title attribute while enter delay is active
this.fixTitle()
this.hoverTimeout = setTimeout(() => {
this.$_hoverTimeout = setTimeout(() => {
/* istanbul ignore else */
if (this.$_hoverState === 'in') {
this.show()
Expand All @@ -917,12 +895,12 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
if (this.isWithActiveTrigger) {
return
}
clearTimeout(this.hoverTimeout)
this.clearHoverTimeout()
this.$_hoverState = 'out'
if (!this.computedDelay.hide) {
this.hide()
} else {
this.$hoverTimeout = setTimeout(() => {
this.$_hoverTimeout = setTimeout(() => {
if (this.$_hoverState === 'out') {
this.hide()
}
Expand Down

0 comments on commit c71352d

Please sign in to comment.