From 8775bacdd9ebc6784b1bbf9d4c7e77b17cbfbe51 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Tue, 12 Nov 2019 05:47:41 -0400 Subject: [PATCH] improve whole-page performance by scoping events handlers (#740) * scope onMouseDown event capture * supercedes #710 * make isIE11 a const * scope keydown * scope mouseover * fix removeEventListener for keyup --- src/scripts/choices.js | 115 +++++++++++++++++++++------------- src/scripts/lib/utils.js | 13 ---- src/scripts/lib/utils.test.js | 13 ---- 3 files changed, 71 insertions(+), 70 deletions(-) diff --git a/src/scripts/choices.js b/src/scripts/choices.js index c9a4cdc..535ec94 100644 --- a/src/scripts/choices.js +++ b/src/scripts/choices.js @@ -36,13 +36,16 @@ import { strToEl, sortByScore, generateId, - findAncestorByAttrName, - isIE11, existsInArray, cloneObject, diff, } from './lib/utils'; +/** @see {@link http://browserhacks.com/#hack-acea075d0ac6954f275a70023906050c} */ +const IS_IE11 = + '-ms-scroll-limit' in document.documentElement.style && + '-ms-ime-align' in document.documentElement.style; + /** * @typedef {import('../../types/index').Choices.Choice} Choice * @typedef {import('../../types/index').Choices.Item} Item @@ -1226,16 +1229,24 @@ class Choices { const { documentElement } = document; // capture events - can cancel event processing or propagation - documentElement.addEventListener('keydown', this._onKeyDown, true); documentElement.addEventListener('touchend', this._onTouchEnd, true); - documentElement.addEventListener('mousedown', this._onMouseDown, true); + this.containerOuter.element.addEventListener( + 'keydown', + this._onKeyDown, + true, + ); + this.containerOuter.element.addEventListener( + 'mousedown', + this._onMouseDown, + true, + ); // passive events - doesn't call `preventDefault` or `stopPropagation` documentElement.addEventListener('click', this._onClick, { passive: true }); documentElement.addEventListener('touchmove', this._onTouchMove, { passive: true, }); - documentElement.addEventListener('mouseover', this._onMouseOver, { + this.dropdown.element.addEventListener('mouseover', this._onMouseOver, { passive: true, }); @@ -1271,20 +1282,28 @@ class Choices { _removeEventListeners() { const { documentElement } = document; - documentElement.removeEventListener('keydown', this._onKeyDown, true); documentElement.removeEventListener('touchend', this._onTouchEnd, true); - documentElement.removeEventListener('mousedown', this._onMouseDown, true); + this.containerOuter.element.removeEventListener( + 'keydown', + this._onKeyDown, + true, + ); + this.containerOuter.element.removeEventListener( + 'mousedown', + this._onMouseDown, + true, + ); - documentElement.removeEventListener('keyup', this._onKeyUp); documentElement.removeEventListener('click', this._onClick); documentElement.removeEventListener('touchmove', this._onTouchMove); - documentElement.removeEventListener('mouseover', this._onMouseOver); + this.dropdown.element.removeEventListener('mouseover', this._onMouseOver); if (this._isSelectOneElement) { this.containerOuter.element.removeEventListener('focus', this._onFocus); this.containerOuter.element.removeEventListener('blur', this._onBlur); } + this.input.element.removeEventListener('keyup', this._onKeyUp); this.input.element.removeEventListener('focus', this._onFocus); this.input.element.removeEventListener('blur', this._onBlur); @@ -1295,13 +1314,13 @@ class Choices { this.input.removeEventListeners(); } + /** + * @param {KeyboardEvent} event + */ _onKeyDown(event) { const { target, keyCode, ctrlKey, metaKey } = event; - if ( - target !== this.input.element && - !this.containerOuter.element.contains(target) - ) { + if (target !== this.input.element) { return; } @@ -1558,47 +1577,55 @@ class Choices { this._wasTap = true; } + /** + * Handles mousedown event in capture mode for containetOuter.element + * @param {MouseEvent} event + */ _onMouseDown(event) { - const { target, shiftKey } = event; - // If we have our mouse down on the scrollbar and are on IE11... - if ( - this.choiceList.element.contains(target) && - isIE11(navigator.userAgent) - ) { - this._isScrollingOnIe = true; - } - - if ( - !this.containerOuter.element.contains(target) || - target === this.input.element - ) { + const { target } = event; + if (!(target instanceof HTMLElement)) { return; } - const { activeItems } = this._store; - const hasShiftKey = shiftKey; - const buttonTarget = findAncestorByAttrName(target, 'data-button'); - const itemTarget = findAncestorByAttrName(target, 'data-item'); - const choiceTarget = findAncestorByAttrName(target, 'data-choice'); - - if (buttonTarget) { - this._handleButtonAction(activeItems, buttonTarget); - } else if (itemTarget) { - this._handleItemAction(activeItems, itemTarget, hasShiftKey); - } else if (choiceTarget) { - this._handleChoiceAction(activeItems, choiceTarget); + // If we have our mouse down on the scrollbar and are on IE11... + if (IS_IE11 && this.choiceList.element.contains(target)) { + // check if click was on a scrollbar area + const firstChoice = /** @type {HTMLElement} */ (this.choiceList.element + .firstElementChild); + const isOnScrollbar = + this._direction === 'ltr' + ? event.offsetX >= firstChoice.offsetWidth + : event.offsetX < firstChoice.offsetLeft; + this._isScrollingOnIe = isOnScrollbar; } + if (target === this.input.element) { + return; + } + + const item = target.closest('[data-button],[data-item],[data-choice]'); + if (item instanceof HTMLElement) { + const hasShiftKey = event.shiftKey; + const { activeItems } = this._store; + const { dataset } = item; + + if ('button' in dataset) { + this._handleButtonAction(activeItems, item); + } else if ('item' in dataset) { + this._handleItemAction(activeItems, item, hasShiftKey); + } else if ('choice' in dataset) { + this._handleChoiceAction(activeItems, item); + } + } event.preventDefault(); } + /** + * Handles mouseover event over this.dropdown + * @param {MouseEvent} event + */ _onMouseOver({ target }) { - const targetWithinDropdown = - target === this.dropdown || this.dropdown.element.contains(target); - const shouldHighlightChoice = - targetWithinDropdown && target.hasAttribute('data-choice'); - - if (shouldHighlightChoice) { + if (target instanceof HTMLElement && 'choice' in target.dataset) { this._highlightChoice(target); } } diff --git a/src/scripts/lib/utils.js b/src/scripts/lib/utils.js index d159d54..8cbd576 100644 --- a/src/scripts/lib/utils.js +++ b/src/scripts/lib/utils.js @@ -58,12 +58,6 @@ export const wrap = (element, wrapper = document.createElement('div')) => { return wrapper.appendChild(element); }; -/** - * @param {HTMLElement} el - * @param {string} attr - */ -export const findAncestorByAttrName = (el, attr) => el.closest(`[${attr}]`); - /** * @param {Element} startEl * @param {string} selector @@ -185,13 +179,6 @@ export const dispatchEvent = (element, type, customArgs = null) => { return element.dispatchEvent(event); }; -/** - * @param {string} userAgent - * @returns {boolean} - */ -export const isIE11 = userAgent => - !!(userAgent.match(/Trident/) && userAgent.match(/rv[ :]11/)); - /** * @param {array} array * @param {any} value diff --git a/src/scripts/lib/utils.test.js b/src/scripts/lib/utils.test.js index b6d633f..2366f56 100644 --- a/src/scripts/lib/utils.test.js +++ b/src/scripts/lib/utils.test.js @@ -9,7 +9,6 @@ import { sanitise, sortByAlpha, sortByScore, - isIE11, existsInArray, cloneObject, dispatchEvent, @@ -202,18 +201,6 @@ describe('utils', () => { }); }); - describe('isIE11', () => { - it('returns whether the given user agent string matches an IE11 user agent string', () => { - const IE11UserAgent = - 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko'; - const firefoxUserAgent = - 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0'; - - expect(isIE11(IE11UserAgent)).to.equal(true); - expect(isIE11(firefoxUserAgent)).to.equal(false); - }); - }); - describe('existsInArray', () => { it('determines whether a value exists within given array', () => { const values = [