Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve whole-page performance by scoping events handlers #740

Merged
merged 7 commits into from
Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 70 additions & 43 deletions src/scripts/choices.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
});

Expand Down Expand Up @@ -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);

Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
const { target } = event;
if (!(target instanceof HTMLElement)) {
return;
}

if (
!this.containerOuter.element.contains(target) ||
target === this.input.element
) {
// 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
jshjohnson marked this conversation as resolved.
Show resolved Hide resolved
.firstElementChild);
const isOnScrollbar =
this._direction === 'ltr'
? event.offsetX >= firstChoice.offsetWidth
: event.offsetX < firstChoice.offsetLeft;
this._isScrollingOnIe = isOnScrollbar;
}

if (target === this.input.element) {
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);
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);
}
}
Expand Down
13 changes: 0 additions & 13 deletions src/scripts/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 0 additions & 13 deletions src/scripts/lib/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
sanitise,
sortByAlpha,
sortByScore,
isIE11,
existsInArray,
cloneObject,
dispatchEvent,
Expand Down Expand Up @@ -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 = [
Expand Down