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

Add event listeners #119

Merged
merged 3 commits into from
Aug 10, 2024
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
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