Skip to content

Commit

Permalink
Add event listeners (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
asamuzaK authored Aug 10, 2024
1 parent 7c6de6e commit 5e80ec4
Show file tree
Hide file tree
Showing 5 changed files with 537 additions and 72 deletions.
6 changes: 4 additions & 2 deletions src/js/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const AN_PLUS_B = 'AnPlusB';
export const COMBINATOR = 'Combinator';
export const EMPTY = '__EMPTY__';
export const IDENTIFIER = 'Identifier';
export const KEY_TAB = 'Tab';
export const NOT_SUPPORTED_ERR = 'NotSupportedError';
export const NTH = 'Nth';
export const RAW = 'Raw';
Expand Down Expand Up @@ -118,9 +119,10 @@ export const REG_SHADOW_MODE = /^(?:close|open)$/;
export const REG_SHADOW_PSEUDO = /^part|slotted$/;
export const REG_TAG_NAME = /[A-Z][\\w-]*/i;
export const REG_TYPE_CHECK = /^(?:checkbox|radio)$/;
export const REG_TYPE_DATE = /^(?:date(?:time-local)?|month|time|week)$/;
export const REG_TYPE_INPUT =
/^(?:date(?:time-local)?|email|month|number|password|search|tel|text|time|url|week)$/;
export const REG_TYPE_RANGE =
/(?:date(?:time-local)?|month|number|range|time|week)$/;
/^(?:date(?:time-local)?|month|number|range|time|week)$/;
export const REG_TYPE_RESET = /^(?:button|reset)$/;
export const REG_TYPE_SUBMIT = /^(?:image|submit)$/;
export const REG_TYPE_TEXT = /^(?:email|number|password|search|tel|text|url)$/;
138 changes: 71 additions & 67 deletions src/js/finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ import {
generateCSS, parseSelector, sortAST, unescapeSelector, walkAST
} from './parser.js';
import {
isContentEditable, isCustomElement, isInShadowTree, resolveContent,
sortNodes, traverseNode
isContentEditable, isCustomElement, isFocusVisible, isFocusable,
isInShadowTree, resolveContent, sortNodes, traverseNode
} from './utility.js';

/* constants */
import {
BIT_01, COMBINATOR, DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, ELEMENT_NODE,
EMPTY, NOT_SUPPORTED_ERR, REG_ANCHOR, REG_FORM, REG_FORM_CTRL,
EMPTY, KEY_TAB, NOT_SUPPORTED_ERR, REG_ANCHOR, REG_FORM, REG_FORM_CTRL,
REG_FORM_VALID, REG_INTERACT, REG_LOGICAL_PSEUDO, REG_SHADOW_HOST,
REG_TYPE_CHECK, REG_TYPE_DATE, REG_TYPE_RANGE, REG_TYPE_RESET,
REG_TYPE_CHECK, REG_TYPE_INPUT, REG_TYPE_RANGE, REG_TYPE_RESET,
REG_TYPE_SUBMIT, REG_TYPE_TEXT, SELECTOR_ATTR, SELECTOR_CLASS, SELECTOR_ID,
SELECTOR_PSEUDO_CLASS, SELECTOR_PSEUDO_ELEMENT, SELECTOR_TYPE, SHOW_ALL,
SYNTAX_ERR, TARGET_ALL, TARGET_FIRST, TARGET_LINEAL, TARGET_SELF, TEXT_NODE,
Expand Down Expand Up @@ -58,6 +58,7 @@ export class Finder {
#document;
#documentCache;
#event;
#focus;
#invalidate;
#invalidateResults;
#matcher;
Expand All @@ -84,6 +85,7 @@ export class Finder {
this.#documentCache = new WeakMap();
this.#invalidateResults = new WeakMap();
this.#results = new WeakMap();
this._registerEventListeners();
}

/**
Expand Down Expand Up @@ -111,6 +113,37 @@ export class Finder {
}
}

/**
* register event listeners
* @private
* @returns {Array.<void>} - results
*/
_registerEventListeners() {
const opt = {
capture: true,
passive: true
};
const func = [];
const mouseKeys = ['mouseover', 'mousedown', 'mouseup', 'mouseout'];
for (const key of mouseKeys) {
func.push(this.#window.addEventListener(key, evt => {
this.#event = evt;
}, opt));
}
const keyboardKeys = ['keydown', 'keyup'];
for (const key of keyboardKeys) {
func.push(this.#window.addEventListener(key, evt => {
if (evt.key === KEY_TAB) {
this.#event = evt;
}
}, opt));
}
func.push(this.#window.addEventListener('focusin', evt => {
this.#focus = evt;
}, opt));
return func;
}

/**
* setup finder
* @param {string} selector - CSS selector
Expand Down Expand Up @@ -938,16 +971,16 @@ export class Finder {
}
case 'hover': {
const { target, type } = this.#event ?? {};
if ((type === 'mouseover' || type === 'pointerover') &&
if (/^(?:mouse|pointer)(?:down|over|up)$/.test(type) &&
node.contains(target)) {
matched.add(node);
}
break;
}
case 'active': {
const { buttons, target, type } = this.#event ?? {};
if ((type === 'mousedown' || type === 'pointerdown') &&
buttons & BIT_01 && node.contains(target)) {
if (/(?:mouse|pointer)down/.test(type) && buttons & BIT_01 &&
node.contains(target)) {
matched.add(node);
}
break;
Expand Down Expand Up @@ -985,78 +1018,53 @@ export class Finder {
}
break;
}
case 'focus':
case 'focus-visible': {
const { target, type } = this.#event ?? {};
case 'focus': {
if (node === this.#document.activeElement && node.tabIndex >= 0 &&
(astName === 'focus' ||
(type === 'keydown' && node.contains(target)))) {
let refNode = node;
let focus = true;
while (refNode) {
if (refNode.disabled || refNode.hasAttribute('disabled') ||
refNode.hidden || refNode.hasAttribute('hidden')) {
focus = false;
break;
isFocusable(node)) {
matched.add(node);
}
break;
}
case 'focus-visible': {
if (node === this.#document.activeElement && node.tabIndex >= 0) {
let bool;
if (isFocusVisible(node)) {
bool = true;
} else {
const { key, target: eventTarget, type } = this.#event ?? {};
if (/^key(?:down|up)$/.test(type) && key === KEY_TAB &&
node.contains(eventTarget)) {
bool = true;
} else {
const { display, visibility } =
this.#window.getComputedStyle(refNode);
focus = !(display === 'none' || visibility === 'hidden');
if (!focus) {
break;
const {
target: focusTarget, relatedTarget
} = this.#focus ?? {};
if (relatedTarget && isFocusVisible(relatedTarget) &&
node.contains(focusTarget)) {
bool = true;
}
}
if (refNode.parentNode &&
refNode.parentNode.nodeType === ELEMENT_NODE) {
refNode = refNode.parentNode;
} else {
break;
}
}
if (focus) {
if (bool && isFocusable(node)) {
matched.add(node);
}
}
break;
}
case 'focus-within': {
let active;
let bool;
let current = this.#document.activeElement;
if (current.tabIndex >= 0) {
while (current) {
if (current === node) {
active = true;
bool = true;
break;
}
current = current.parentNode;
}
}
if (active) {
let refNode = node;
let focus = true;
while (refNode) {
if (refNode.disabled || refNode.hasAttribute('disabled') ||
refNode.hidden || refNode.hasAttribute('hidden')) {
focus = false;
break;
} else {
const { display, visibility } =
this.#window.getComputedStyle(refNode);
focus = !(display === 'none' || visibility === 'hidden');
if (!focus) {
break;
}
}
if (refNode.parentNode &&
refNode.parentNode.nodeType === ELEMENT_NODE) {
refNode = refNode.parentNode;
} else {
break;
}
}
if (focus) {
matched.add(node);
}
if (bool && isFocusable(node)) {
matched.add(node);
}
break;
}
Expand Down Expand Up @@ -1189,8 +1197,7 @@ export class Finder {
break;
}
case 'input': {
if ((!node.type || REG_TYPE_DATE.test(node.type) ||
REG_TYPE_TEXT.test(node.type)) &&
if ((!node.type || REG_TYPE_INPUT.test(node.type)) &&
(node.readonly || node.hasAttribute('readonly') ||
node.disabled || node.hasAttribute('disabled'))) {
matched.add(node);
Expand All @@ -1215,8 +1222,7 @@ export class Finder {
break;
}
case 'input': {
if ((!node.type || REG_TYPE_DATE.test(node.type) ||
REG_TYPE_TEXT.test(node.type)) &&
if ((!node.type || REG_TYPE_INPUT.test(node.type)) &&
!(node.readonly || node.hasAttribute('readonly') ||
node.disabled || node.hasAttribute('disabled'))) {
matched.add(node);
Expand Down Expand Up @@ -1473,8 +1479,7 @@ export class Finder {
if (node.hasAttribute('type')) {
const inputType = node.getAttribute('type');
if (inputType === 'file' || REG_TYPE_CHECK.test(inputType) ||
REG_TYPE_DATE.test(inputType) ||
REG_TYPE_TEXT.test(inputType)) {
REG_TYPE_INPUT.test(inputType)) {
targetNode = node;
}
} else {
Expand All @@ -1495,8 +1500,7 @@ export class Finder {
if (node.hasAttribute('type')) {
const inputType = node.getAttribute('type');
if (inputType === 'file' || REG_TYPE_CHECK.test(inputType) ||
REG_TYPE_DATE.test(inputType) ||
REG_TYPE_TEXT.test(inputType)) {
REG_TYPE_INPUT.test(inputType)) {
targetNode = node;
}
} else {
Expand Down
66 changes: 64 additions & 2 deletions src/js/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import isCustomElementName from 'is-potential-custom-element-name';
import {
DOCUMENT_FRAGMENT_NODE, DOCUMENT_NODE, DOCUMENT_POSITION_CONTAINS,
DOCUMENT_POSITION_PRECEDING, ELEMENT_NODE, REG_DIR, REG_FILTER_COMPLEX,
REG_FILTER_COMPOUND, REG_FILTER_SIMPLE, REG_SHADOW_MODE, TEXT_NODE,
TYPE_FROM, TYPE_TO, WALKER_FILTER
REG_FILTER_COMPOUND, REG_FILTER_SIMPLE, REG_SHADOW_MODE, REG_TYPE_INPUT,
TEXT_NODE, TYPE_FROM, TYPE_TO, WALKER_FILTER
} from './constant.js';

/**
Expand Down Expand Up @@ -366,6 +366,68 @@ export const isContentEditable = node => {
return !!res;
};

/**
* is focus visible
* @param {object} node - Element node
* @returns {boolean} - result
*/
export const isFocusVisible = node => {
let res;
if (node?.nodeType === ELEMENT_NODE) {
const { localName, type } = node;
switch (localName) {
case 'input': {
if (!type || REG_TYPE_INPUT.test(type)) {
res = true;
}
break;
}
case 'textarea': {
res = true;
break;
}
default: {
res = isContentEditable(node);
}
}
}
return !!res;
};

/**
* is focusable
* NOTE: workaround for jsdom issue: https://github.com/jsdom/jsdom/issues/3464
* @param {object} node - Element node
* @returns {boolean} - result
*/
export const isFocusable = node => {
let res;
if (node?.nodeType === ELEMENT_NODE) {
const window = node.ownerDocument.defaultView;
let refNode = node;
res = true;
while (refNode) {
if (refNode.disabled || refNode.hasAttribute('disabled') ||
refNode.hidden || refNode.hasAttribute('hidden')) {
res = false;
break;
} else {
const { display, visibility } = window.getComputedStyle(refNode);
res = !(display === 'none' || visibility === 'hidden');
if (!res) {
break;
}
}
if (refNode.parentNode && refNode.parentNode.nodeType === ELEMENT_NODE) {
refNode = refNode.parentNode;
} else {
break;
}
}
}
return !!res;
};

/**
* get namespace URI
* @param {string} ns - namespace prefix
Expand Down
Loading

0 comments on commit 5e80ec4

Please sign in to comment.