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
This commit is contained in:
Konstantin Vyatkin 2019-11-12 05:47:41 -04:00 committed by Josh Johnson
parent fb2310cb56
commit 8775bacdd9
3 changed files with 71 additions and 70 deletions

View file

@ -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);
}
}

View file

@ -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

View file

@ -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 = [