diff --git a/src/components/carousel/carousel.js b/src/components/carousel/carousel.js index 775456cc7a6..67a7ca5e534 100644 --- a/src/components/carousel/carousel.js +++ b/src/components/carousel/carousel.js @@ -205,9 +205,10 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({ }, created() { // Create private non-reactive props - this._intervalId = null - this._animationTimeout = null - this._touchTimeout = null + this.$_interval = null + this.$_animationTimeout = null + this.$_touchTimeout = null + this.$_observer = null // Set initial paused state this.isPaused = !(toInteger(this.interval, 0) > 0) }, @@ -217,22 +218,39 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({ // Get all slides this.updateSlides() // Observe child changes so we can update slide list - observeDom(this.$refs.inner, this.updateSlides.bind(this), { - subtree: false, - childList: true, - attributes: true, - attributeFilter: ['id'] - }) + this.setObserver(true) }, beforeDestroy() { - clearTimeout(this._animationTimeout) - clearTimeout(this._touchTimeout) - clearInterval(this._intervalId) - this._intervalId = null - this._animationTimeout = null - this._touchTimeout = null + this.clearInterval() + this.clearAnimationTimeout() + this.clearTouchTimeout() + this.setObserver(false) }, methods: { + clearInterval() { + clearInterval(this.$_interval) + this.$_interval = null + }, + clearAnimationTimeout() { + clearTimeout(this.$_animationTimeout) + this.$_animationTimeout = null + }, + clearTouchTimeout() { + clearTimeout(this.$_touchTimeout) + this.$_touchTimeout = null + }, + setObserver(on = false) { + this.$_observer && this.$_observer.disconnect() + this.$_observer = null + if (on) { + this.$_observer = observeDom(this.$refs.inner, this.updateSlides.bind(this), { + subtree: false, + childList: true, + attributes: true, + attributeFilter: ['id'] + }) + } + }, // Set slide setSlide(slide, direction = null) { // Don't animate when page is not visible @@ -286,10 +304,7 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({ if (!evt) { this.isPaused = true } - if (this._intervalId) { - clearInterval(this._intervalId) - this._intervalId = null - } + this.clearInterval() }, // Start auto rotate slides start(evt) { @@ -297,13 +312,10 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({ this.isPaused = false } /* istanbul ignore next: most likely will never happen, but just in case */ - if (this._intervalId) { - clearInterval(this._intervalId) - this._intervalId = null - } + this.clearInterval() // Don't start if no interval, or less than 2 slides if (this.interval && this.numSlides > 1) { - this._intervalId = setInterval(this.next, mathMax(1000, this.interval)) + this.$_interval = setInterval(this.next, mathMax(1000, this.interval)) } }, // Restart auto rotate slides when focus/hover leaves the carousel @@ -362,7 +374,7 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({ eventOff(currentSlide, evt, onceTransEnd, EVENT_OPTIONS_NO_CAPTURE) ) } - this._animationTimeout = null + this.clearAnimationTimeout() removeClass(nextSlide, dirClass) removeClass(nextSlide, overlayClass) addClass(nextSlide, 'active') @@ -387,7 +399,7 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({ ) } // Fallback to setTimeout() - this._animationTimeout = setTimeout(onceTransEnd, TRANS_DURATION) + this.$_animationTimeout = setTimeout(onceTransEnd, TRANS_DURATION) } if (isCycling) { this.start(false) @@ -480,10 +492,8 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({ // is NOT fired) and after a timeout (to allow for mouse compatibility // events to fire) we explicitly restart cycling this.pause(false) - if (this._touchTimeout) { - clearTimeout(this._touchTimeout) - } - this._touchTimeout = setTimeout( + this.clearTouchTimeout() + this.$_touchTimeout = setTimeout( this.start, TOUCH_EVENT_COMPAT_WAIT + mathMax(1000, this.interval) ) diff --git a/src/components/form-spinbutton/form-spinbutton.js b/src/components/form-spinbutton/form-spinbutton.js index 47ba175c8f0..54ccbd3a209 100644 --- a/src/components/form-spinbutton/form-spinbutton.js +++ b/src/components/form-spinbutton/form-spinbutton.js @@ -466,6 +466,8 @@ export const BFormSpinbutton = /*#__PURE__*/ Vue.extend({ resetTimers() { clearTimeout(this.$_autoDelayTimer) clearInterval(this.$_autoRepeatTimer) + this.$_autoDelayTimer = null + this.$_autoRepeatTimer = null }, clearRepeat() { this.resetTimers() diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index a2363652ec4..36ce83a9e2b 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -450,7 +450,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ }, created() { // Define non-reactive properties - this._observer = null + this.$_observer = null }, mounted() { // Set initial z-index as queried from the DOM @@ -470,10 +470,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ }, beforeDestroy() { // Ensure everything is back to normal - if (this._observer) { - this._observer.disconnect() - this._observer = null - } + this.setObserver(false) if (this.isVisible) { this.isVisible = false this.isShow = false @@ -481,6 +478,17 @@ export const BModal = /*#__PURE__*/ Vue.extend({ } }, methods: { + setObserver(on = false) { + this.$_observer && this.$_observer.disconnect() + this.$_observer = null + if (on) { + this.$_observer = observeDom( + this.$refs.content, + this.checkModalOverflow.bind(this), + OBSERVER_CONFIG + ) + } + }, // Private method to update the v-model updateModel(val) { if (val !== this.visible) { @@ -562,10 +570,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({ return } // Stop observing for content changes - if (this._observer) { - this._observer.disconnect() - this._observer = null - } + this.setObserver(false) // Trigger the hide transition this.isVisible = false // Update the v-model @@ -615,13 +620,9 @@ export const BModal = /*#__PURE__*/ Vue.extend({ // Update the v-model this.updateModel(true) this.$nextTick(() => { - // In a nextTick in case modal content is lazy // Observe changes in modal content and adjust if necessary - this._observer = observeDom( - this.$refs.content, - this.checkModalOverflow.bind(this), - OBSERVER_CONFIG - ) + // In a `$nextTick()` in case modal content is lazy + this.setObserver(true) }) }) }, diff --git a/src/components/table/helpers/mixin-filtering.js b/src/components/table/helpers/mixin-filtering.js index 2e1d5b36195..deb4646bec6 100644 --- a/src/components/table/helpers/mixin-filtering.js +++ b/src/components/table/helpers/mixin-filtering.js @@ -101,8 +101,7 @@ export default { // Watch for debounce being set to 0 computedFilterDebounce(newVal) { if (!newVal && this.$_filterTimer) { - clearTimeout(this.$_filterTimer) - this.$_filterTimer = null + this.clearFilterTimer() this.localFilter = this.filterSanitize(this.filter) } }, @@ -113,8 +112,7 @@ export default { deep: true, handler(newCriteria) { const timeout = this.computedFilterDebounce - clearTimeout(this.$_filterTimer) - this.$_filterTimer = null + this.clearFilterTimer() if (timeout && timeout > 0) { // If we have a debounce time, delay the update of `localFilter` this.$_filterTimer = setTimeout(() => { @@ -155,7 +153,7 @@ export default { } }, created() { - // Create non-reactive prop where we store the debounce timer id + // Create private non-reactive props this.$_filterTimer = null // If filter is "pre-set", set the criteria // This will trigger any watchers/dependents @@ -167,10 +165,13 @@ export default { }) }, beforeDestroy() /* istanbul ignore next */ { - clearTimeout(this.$_filterTimer) - this.$_filterTimer = null + this.clearFilterTimer() }, methods: { + clearFilterTimer() { + clearTimeout(this.$_filterTimer) + this.$_filterTimer = null + }, filterSanitize(criteria) { // Sanitizes filter criteria based on internal or external filtering if ( diff --git a/src/components/table/helpers/mixin-tbody.js b/src/components/table/helpers/mixin-tbody.js index 6518aaf9985..2a5d4c38647 100644 --- a/src/components/table/helpers/mixin-tbody.js +++ b/src/components/table/helpers/mixin-tbody.js @@ -17,6 +17,9 @@ const props = { export default { mixins: [tbodyRowMixin], props, + beforeDestroy() { + this.$_bodyFieldSlotNameCache = null + }, methods: { // Helper methods getTbodyTrs() { diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index eb53b56c435..75008b57eab 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -312,8 +312,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, created() { + // Create private non-reactive props + this.$_observer = null this.currentTab = toInteger(this.value, -1) - this._bvObserver = null // For SSR and to make sure only a single tab is shown on mount // We wrap this in a `$nextTick()` to ensure the child tabs have been created this.$nextTick(() => { @@ -362,11 +363,11 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ unregisterTab(tab) { this.registeredTabs = this.registeredTabs.slice().filter(t => t !== tab) }, + // DOM observer is needed to detect changes in order of tabs setObserver(on) { - // DOM observer is needed to detect changes in order of tabs + this.$_observer && this.$_observer.disconnect() + this.$_observer = null if (on) { - // Make sure no existing observer running - this.setObserver(false) const self = this /* istanbul ignore next: difficult to test mutation observer in JSDOM */ const handler = () => { @@ -379,18 +380,12 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ }) } // Watch for changes to sub components - this._bvObserver = observeDom(this.$refs.tabsContainer, handler, { + this.$_observer = observeDom(this.$refs.tabsContainer, handler, { childList: true, subtree: false, attributes: true, attributeFilter: ['id'] }) - } else { - /* istanbul ignore next */ - if (this._bvObserver && this._bvObserver.disconnect) { - this._bvObserver.disconnect() - } - this._bvObserver = null } }, getTabs() { diff --git a/src/components/tooltip/helpers/bv-popper.js b/src/components/tooltip/helpers/bv-popper.js index a1360184c50..2cace4baa7b 100644 --- a/src/components/tooltip/helpers/bv-popper.js +++ b/src/components/tooltip/helpers/bv-popper.js @@ -157,10 +157,10 @@ export const BVPopper = /*#__PURE__*/ Vue.extend({ updated() { // Update popper if needed // TODO: Should this be a watcher on `this.popperConfig` instead? - this.popperUpdate() + this.updatePopper() }, beforeDestroy() { - this.popperDestroy() + this.destroyPopper() }, destroyed() { // Make sure template is removed from DOM @@ -198,16 +198,16 @@ export const BVPopper = /*#__PURE__*/ Vue.extend({ return this.offset }, popperCreate(el) { - this.popperDestroy() + this.destroyPopper() // We use `el` rather than `this.$el` just in case the original // mountpoint root element type was changed by the template this.$_popper = new Popper(this.target, el, this.popperConfig) }, - popperDestroy() { + destroyPopper() { this.$_popper && this.$_popper.destroy() this.$_popper = null }, - popperUpdate() { + updatePopper() { this.$_popper && this.$_popper.scheduleUpdate() }, popperPlacementChange(data) { diff --git a/src/components/tooltip/helpers/bv-tooltip.js b/src/components/tooltip/helpers/bv-tooltip.js index c7800d74b60..656dbfca221 100644 --- a/src/components/tooltip/helpers/bv-tooltip.js +++ b/src/components/tooltip/helpers/bv-tooltip.js @@ -343,7 +343,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({ this.clearActiveTriggers() this.localPlacementTarget = null try { - this.$_tip && this.$_tip.$destroy() + this.$_tip.$destroy() } catch {} this.$_tip = null this.removeAriaDescribedby() @@ -552,16 +552,12 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({ return this.isDropdown() && target && select(DROPDOWN_OPEN_SELECTOR, target) }, clearHoverTimeout() { - if (this.$_hoverTimeout) { - clearTimeout(this.$_hoverTimeout) - this.$_hoverTimeout = null - } + clearTimeout(this.$_hoverTimeout) + this.$_hoverTimeout = null }, clearVisibilityInterval() { - if (this.$_visibleInterval) { - clearInterval(this.$_visibleInterval) - this.$_visibleInterval = null - } + clearInterval(this.$_visibleInterval) + this.$_visibleInterval = null }, clearActiveTriggers() { for (const trigger in this.activeTrigger) { diff --git a/src/components/tooltip/tooltip.js b/src/components/tooltip/tooltip.js index 512b38625d1..31ccc1c926a 100644 --- a/src/components/tooltip/tooltip.js +++ b/src/components/tooltip/tooltip.js @@ -144,12 +144,12 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({ }, watch: { show(show, oldVal) { - if (show !== oldVal && show !== this.localShow && this.$_bv_toolpop) { + if (show !== oldVal && show !== this.localShow && this.$_toolpop) { if (show) { - this.$_bv_toolpop.show() + this.$_toolpop.show() } else { // We use `forceHide()` to override any active triggers - this.$_bv_toolpop.forceHide() + this.$_toolpop.forceHide() } } }, @@ -166,8 +166,8 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({ }, templateData() { this.$nextTick(() => { - if (this.$_bv_toolpop) { - this.$_bv_toolpop.updateData(this.templateData) + if (this.$_toolpop) { + this.$_toolpop.updateData(this.templateData) } }) }, @@ -177,8 +177,8 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({ } }, created() { - // Non reactive properties - this.$_bv_toolpop = null + // Create private non-reactive props + this.$_toolpop = null }, updated() { // Update the `propData` object @@ -192,8 +192,10 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({ this.$off('disable', this.doDisable) this.$off('enable', this.doEnable) // Destroy the tip instance - this.$_bv_toolpop && this.$_bv_toolpop.$destroy() - this.$_bv_toolpop = null + if (this.$_toolpop) { + this.$_toolpop.$destroy() + this.$_toolpop = null + } }, mounted() { // Instantiate a new BVTooltip instance @@ -207,7 +209,7 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({ // Pass down the scoped style attribute if available const scopeId = getScopId(this) || getScopId(this.$parent) // Create the instance - const $toolpop = (this.$_bv_toolpop = new Component({ + const $toolpop = (this.$_toolpop = new Component({ parent: this, // Pass down the scoped style ID _scopeId: scopeId || undefined @@ -236,7 +238,7 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({ this.$on('enable', this.doEnable) // Initially show tooltip? if (this.localShow) { - this.$_bv_toolpop && this.$_bv_toolpop.show() + $toolpop.show() } }) }, @@ -307,16 +309,16 @@ export const BTooltip = /*#__PURE__*/ Vue.extend({ }, // --- Local event listeners --- doOpen() { - !this.localShow && this.$_bv_toolpop && this.$_bv_toolpop.show() + !this.localShow && this.$_toolpop && this.$_toolpop.show() }, doClose() { - this.localShow && this.$_bv_toolpop && this.$_bv_toolpop.hide() + this.localShow && this.$_toolpop && this.$_toolpop.hide() }, doDisable() { - this.$_bv_toolpop && this.$_bv_toolpop.disable() + this.$_toolpop && this.$_toolpop.disable() }, doEnable() { - this.$_bv_toolpop && this.$_bv_toolpop.enable() + this.$_toolpop && this.$_toolpop.enable() } }, render(h) { diff --git a/src/directives/scrollspy/scrollspy.class.js b/src/directives/scrollspy/scrollspy.class.js index 65c82b382ad..4522789b4c1 100644 --- a/src/directives/scrollspy/scrollspy.class.js +++ b/src/directives/scrollspy/scrollspy.class.js @@ -134,8 +134,8 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ { this.$activeTarget = null this.$scrollHeight = 0 this.$resizeTimeout = null - this.$obs_scroller = null - this.$obs_targets = null + this.$scrollerObserver = null + this.$targetsObserver = null this.$root = $root || null this.$config = null @@ -223,16 +223,12 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ { setObservers(on) { // We observe both the scroller for content changes, and the target links - if (this.$obs_scroller) { - this.$obs_scroller.disconnect() - this.$obs_scroller = null - } - if (this.$obs_targets) { - this.$obs_targets.disconnect() - this.$obs_targets = null - } + this.$scrollerObserver && this.$scrollerObserver.disconnect() + this.$targetsObserver && this.$targetsObserver.disconnect() + this.$scrollerObserver = null + this.$targetsObserver = null if (on) { - this.$obs_targets = observeDom( + this.$targetsObserver = observeDom( this.$el, () => { this.handleEvent('mutation') @@ -244,7 +240,7 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ { attributeFilter: ['href'] } ) - this.$obs_scroller = observeDom( + this.$scrollerObserver = observeDom( this.getScroller(), () => { this.handleEvent('mutation') @@ -276,7 +272,7 @@ class ScrollSpy /* istanbul ignore next: not easy to test */ { } if (type === 'scroll') { - if (!this.$obs_scroller) { + if (!this.$scrollerObserver) { // Just in case we are added to the DOM before the scroll target is // We re-instantiate our listeners, just in case this.listen() diff --git a/src/directives/visible/visible.js b/src/directives/visible/visible.js index ec7889f945e..f17d1cc0465 100644 --- a/src/directives/visible/visible.js +++ b/src/directives/visible/visible.js @@ -38,7 +38,7 @@ import { clone, keys } from '../../utils/object' const OBSERVER_PROP_NAME = '__bv__visibility_observer' -const onlyDgitsRE = /^\d+$/ +const RX_ONLY_DIGITS = /^\d+$/ class VisibilityObserver { constructor(el, options, vnode) { @@ -114,11 +114,8 @@ class VisibilityObserver { } stop() { - const observer = this.observer /* istanbul ignore next */ - if (observer && observer.disconnect) { - observer.disconnect() - } + this.observer && this.observer.disconnect() this.observer = null } } @@ -141,7 +138,7 @@ const bind = (el, { value, modifiers }, vnode) => { // Parse modifiers keys(modifiers).forEach(mod => { /* istanbul ignore else: Until is switched to use this directive */ - if (onlyDgitsRE.test(mod)) { + if (RX_ONLY_DIGITS.test(mod)) { options.margin = `${mod}px` } else if (mod.toLowerCase() === 'once') { options.once = true diff --git a/src/mixins/dropdown.js b/src/mixins/dropdown.js index cc92f840b6c..2068babd9ca 100644 --- a/src/mixins/dropdown.js +++ b/src/mixins/dropdown.js @@ -169,7 +169,7 @@ export default { } }, created() { - // Create non-reactive property + // Create private non-reactive props this.$_popper = null }, /* istanbul ignore next */ @@ -236,16 +236,14 @@ export default { this.destroyPopper() this.$_popper = new Popper(element, this.$refs.menu, this.getPopperConfig()) }, + // Ensure popper event listeners are removed cleanly destroyPopper() { - // Ensure popper event listeners are removed cleanly - if (this.$_popper) { - this.$_popper.destroy() - } + this.$_popper && this.$_popper.destroy() this.$_popper = null }, + // Instructs popper to re-computes the dropdown position + // useful if the content changes size updatePopper() /* istanbul ignore next: not easy to test */ { - // Instructs popper to re-computes the dropdown position - // useful if the content changes size try { this.$_popper.scheduleUpdate() } catch {} diff --git a/src/mixins/form-text.js b/src/mixins/form-text.js index cc30ea4cdac..321dd5ef73b 100644 --- a/src/mixins/form-text.js +++ b/src/mixins/form-text.js @@ -117,9 +117,12 @@ export default { } } }, - mounted() { - // Create non-reactive property and set up destroy handler + created() { + // Create private non-reactive props this.$_inputDebounceTimer = null + }, + mounted() { + // Set up destroy handler this.$on('hook:beforeDestroy', this.clearDebounce) // Preset the internal state const value = this.value diff --git a/src/utils/transporter.js b/src/utils/transporter.js index cfabaed40cd..c0ea07d15b9 100644 --- a/src/utils/transporter.js +++ b/src/utils/transporter.js @@ -88,8 +88,9 @@ export const BTransporterSingle = /*#__PURE__*/ Vue.extend({ } }, created() { - this._bv_defaultFn = null - this._bv_target = null + // Create private non-reactive props + this.$_defaultFn = null + this.$_target = null }, beforeMount() { this.mountTarget() @@ -105,7 +106,7 @@ export const BTransporterSingle = /*#__PURE__*/ Vue.extend({ }, beforeDestroy() { this.unmountTarget() - this._bv_defaultFn = null + this.$_defaultFn = null }, methods: { // Get the element which the target should be appended to @@ -120,12 +121,12 @@ export const BTransporterSingle = /*#__PURE__*/ Vue.extend({ }, // Mount the target mountTarget() { - if (!this._bv_target) { + if (!this.$_target) { const container = this.getContainer() if (container) { const el = document.createElement('div') container.appendChild(el) - this._bv_target = new BTransporterTargetSingle({ + this.$_target = new BTransporterTargetSingle({ el, parent: this, propsData: { @@ -138,30 +139,28 @@ export const BTransporterSingle = /*#__PURE__*/ Vue.extend({ }, // Update the content of the target updateTarget() { - if (isBrowser && this._bv_target) { + if (isBrowser && this.$_target) { const defaultFn = this.$scopedSlots.default if (!this.disabled) { /* istanbul ignore else: only applicable in Vue 2.5.x */ - if (defaultFn && this._bv_defaultFn !== defaultFn) { + if (defaultFn && this.$_defaultFn !== defaultFn) { // We only update the target component if the scoped slot // function is a fresh one. The new slot syntax (since Vue 2.6) // can cache unchanged slot functions and we want to respect that here - this._bv_target.updatedNodes = defaultFn + this.$_target.updatedNodes = defaultFn } else if (!defaultFn) { // We also need to be back compatible with non-scoped default slot (i.e. 2.5.x) - this._bv_target.updatedNodes = this.$slots.default + this.$_target.updatedNodes = this.$slots.default } } // Update the scoped slot function cache - this._bv_defaultFn = defaultFn + this.$_defaultFn = defaultFn } }, // Unmount the target unmountTarget() { - if (this._bv_target) { - this._bv_target.$destroy() - this._bv_target = null - } + this.$_target && this.$_target.$destroy() + this.$_target = null } }, render(h) {