happy-dom
Version:
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.
458 lines • 18.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const DOMException_js_1 = __importDefault(require("../exception/DOMException.cjs"));
const PropertySymbol = __importStar(require("../PropertySymbol.cjs"));
const SelectorCombinatorEnum_js_1 = __importDefault(require("./SelectorCombinatorEnum.cjs"));
const SPACE_REGEXP = /\s+/;
/**
* Selector item.
*/
class SelectorItem {
tagName;
id;
classNames;
attributes;
pseudos;
isPseudoElement;
combinator;
ignoreErrors;
/**
* Constructor.
*
* @param [options] Options.
* @param [options.combinator] Combinator.
* @param [options.tagName] Tag name.
* @param [options.id] ID.
* @param [options.classNames] Class names.
* @param [options.attributes] Attributes.
* @param [options.pseudos] Pseudos.
* @param [options.isPseudoElement] Is pseudo element.
* @param [options.ignoreErrors] Ignore errors.
*/
constructor(options) {
this.tagName = options?.tagName || null;
this.id = options?.id || null;
this.classNames = options?.classNames || null;
this.attributes = options?.attributes || null;
this.pseudos = options?.pseudos || null;
this.isPseudoElement = options?.isPseudoElement || false;
this.combinator = options?.combinator || SelectorCombinatorEnum_js_1.default.descendant;
this.ignoreErrors = options?.ignoreErrors || false;
}
/**
* Matches a selector against an element.
*
* @param element HTML element.
* @returns Result.
*/
match(element) {
let priorityWeight = 0;
if (this.isPseudoElement) {
return null;
}
// Tag name match
if (this.tagName) {
if (this.tagName !== '*' && this.tagName !== element[PropertySymbol.tagName].toUpperCase()) {
return null;
}
priorityWeight += 1;
}
// ID Match
if (this.id) {
if (this.id !== element.id) {
return null;
}
priorityWeight += 100;
}
// Class match
if (this.classNames) {
const result = this.matchClass(element);
if (!result) {
return null;
}
priorityWeight += result.priorityWeight;
}
// Attribute match
if (this.attributes) {
const result = this.matchAttributes(element);
if (!result) {
return null;
}
priorityWeight += result.priorityWeight;
}
// Pseudo match
if (this.pseudos) {
const result = this.matchPseudo(element);
if (!result) {
return null;
}
priorityWeight += result.priorityWeight;
}
return { priorityWeight };
}
/**
* Matches a pseudo selector.
*
* @param element Element.
* @returns Result.
*/
matchPseudo(element) {
const parent = element[PropertySymbol.parentNode];
const parentChildren = element[PropertySymbol.parentNode]
? element[PropertySymbol.parentNode][PropertySymbol.elementArray]
: [];
if (!this.pseudos) {
return { priorityWeight: 0 };
}
let priorityWeight = 0;
for (const pseudo of this.pseudos) {
// Validation
switch (pseudo.name) {
case 'not':
case 'nth-child':
case 'nth-of-type':
case 'nth-last-child':
case 'nth-last-of-type':
case 'is':
case 'where':
if (!pseudo.arguments) {
if (this.ignoreErrors) {
return null;
}
throw new DOMException_js_1.default(`Failed to execute 'matches' on '${element.constructor.name}': '${this.getSelectorString()}' is not a valid selector.`);
}
break;
}
// Check if parent exists
if (!parent) {
switch (pseudo.name) {
case 'first-child':
case 'last-child':
case 'only-child':
case 'first-of-type':
case 'last-of-type':
case 'only-of-type':
case 'nth-child':
case 'nth-of-type':
case 'nth-last-child':
case 'nth-last-of-type':
return null;
}
}
const selectorMatch = this.matchPseudoItem(element, parentChildren, pseudo);
if (!selectorMatch) {
return null;
}
priorityWeight += selectorMatch.priorityWeight;
}
return { priorityWeight };
}
/**
* Matches a pseudo selector.
*
* @param element Element.
* @param parentChildren Parent children.
* @param pseudo Pseudo.
*/
matchPseudoItem(element, parentChildren, pseudo) {
switch (pseudo.name) {
case 'first-child':
return parentChildren[0] === element ? { priorityWeight: 10 } : null;
case 'last-child':
return parentChildren.length && parentChildren[parentChildren.length - 1] === element
? { priorityWeight: 10 }
: null;
case 'only-child':
return parentChildren.length === 1 && parentChildren[0] === element
? { priorityWeight: 10 }
: null;
case 'first-of-type':
for (const child of parentChildren) {
if (child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
return child === element ? { priorityWeight: 10 } : null;
}
}
return null;
case 'last-of-type':
for (let i = parentChildren.length - 1; i >= 0; i--) {
const child = parentChildren[i];
if (child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
return child === element ? { priorityWeight: 10 } : null;
}
}
return null;
case 'only-of-type':
let isFound = false;
for (const child of parentChildren) {
if (child[PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
if (isFound || child !== element) {
return null;
}
isFound = true;
}
}
return isFound ? { priorityWeight: 10 } : null;
case 'checked':
return element[PropertySymbol.tagName] === 'INPUT' && element.checked
? { priorityWeight: 10 }
: null;
case 'disabled':
return 'disabled' in element && element.hasAttribute('disabled')
? { priorityWeight: 10 }
: null;
case 'empty':
return !element[PropertySymbol.elementArray].length
? { priorityWeight: 10 }
: null;
case 'root':
return element[PropertySymbol.tagName] === 'HTML' ? { priorityWeight: 10 } : null;
case 'not':
for (const selectorItem of pseudo.selectorItems) {
if (selectorItem.match(element)) {
return null;
}
}
return { priorityWeight: 10 };
case 'nth-child':
let nthChildIndex = -1;
if (pseudo.selectorItems[0] && !pseudo.selectorItems[0].match(element)) {
return null;
}
for (let i = 0, max = parentChildren.length; i < max; i++) {
if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) {
nthChildIndex++;
}
if (parentChildren[i] === element) {
return nthChildIndex !== -1 && pseudo.nthFunction(nthChildIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'nth-of-type':
let nthOfTypeIndex = -1;
for (let i = 0, max = parentChildren.length; i < max; i++) {
if (parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
nthOfTypeIndex++;
}
if (parentChildren[i] === element) {
return nthOfTypeIndex !== -1 && pseudo.nthFunction(nthOfTypeIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'nth-last-child':
let nthLastChildIndex = -1;
if (pseudo.selectorItems[0] && !pseudo.selectorItems[0].match(element)) {
return null;
}
for (let i = parentChildren.length - 1; i >= 0; i--) {
if (!pseudo.selectorItems[0] || pseudo.selectorItems[0].match(parentChildren[i])) {
nthLastChildIndex++;
}
if (parentChildren[i] === element) {
return nthLastChildIndex !== -1 && pseudo.nthFunction(nthLastChildIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'nth-last-of-type':
let nthLastOfTypeIndex = -1;
for (let i = parentChildren.length - 1; i >= 0; i--) {
if (parentChildren[i][PropertySymbol.tagName] === element[PropertySymbol.tagName]) {
nthLastOfTypeIndex++;
}
if (parentChildren[i] === element) {
return nthLastOfTypeIndex !== -1 && pseudo.nthFunction(nthLastOfTypeIndex + 1)
? { priorityWeight: 10 }
: null;
}
}
return null;
case 'target':
const hash = element[PropertySymbol.ownerDocument].location.hash;
if (!hash) {
return null;
}
return element.isConnected && element.id === hash.slice(1) ? { priorityWeight: 10 } : null;
case 'is':
let priorityWeightForIs = 0;
for (const selectorItem of pseudo.selectorItems) {
const match = selectorItem.match(element);
if (match && priorityWeightForIs < match.priorityWeight) {
priorityWeightForIs = match.priorityWeight;
}
}
return priorityWeightForIs ? { priorityWeight: priorityWeightForIs } : null;
case 'where':
for (const selectorItem of pseudo.selectorItems) {
if (selectorItem.match(element)) {
return { priorityWeight: 0 };
}
}
return null;
case 'has':
let priorityWeightForHas = 0;
if (pseudo.arguments[0] === '+') {
const nextSibling = element.nextElementSibling;
if (!nextSibling) {
return null;
}
for (const selectorItem of pseudo.selectorItems) {
const match = selectorItem.match(nextSibling);
if (match && priorityWeightForHas < match.priorityWeight) {
priorityWeightForHas = match.priorityWeight;
}
}
}
else if (pseudo.arguments[0] === '>') {
for (const selectorItem of pseudo.selectorItems) {
for (const child of element[PropertySymbol.elementArray]) {
const match = selectorItem.match(child);
if (match && priorityWeightForHas < match.priorityWeight) {
priorityWeightForHas = match.priorityWeight;
break;
}
}
}
}
else {
for (const selectorItem of pseudo.selectorItems) {
const match = this.matchChildOfElement(selectorItem, element);
if (match && priorityWeightForHas < match.priorityWeight) {
priorityWeightForHas = match.priorityWeight;
}
}
}
return priorityWeightForHas ? { priorityWeight: priorityWeightForHas } : null;
case 'focus':
case 'focus-visible':
return element[PropertySymbol.ownerDocument].activeElement === element
? { priorityWeight: 10 }
: null;
default:
return null;
}
}
/**
* Matches attribute.
*
* @param element Element.
* @returns Result.
*/
matchAttributes(element) {
if (!this.attributes) {
return null;
}
let priorityWeight = 0;
for (const attribute of this.attributes) {
const elementAttribute = element[PropertySymbol.attributes].getNamedItem(attribute.name);
if (!elementAttribute) {
return null;
}
priorityWeight += 10;
if (attribute.value !== null &&
(elementAttribute[PropertySymbol.value] === null ||
(attribute.regExp && !attribute.regExp.test(elementAttribute[PropertySymbol.value])) ||
(!attribute.regExp && attribute.value !== elementAttribute[PropertySymbol.value]))) {
return null;
}
}
return { priorityWeight };
}
/**
* Matches class.
*
* @param element Element.
* @returns Result.
*/
matchClass(element) {
if (!this.classNames) {
return null;
}
const classList = element.className.split(SPACE_REGEXP);
let priorityWeight = 0;
for (const className of this.classNames) {
if (!classList.includes(className)) {
return null;
}
priorityWeight += 10;
}
return { priorityWeight };
}
/**
* Matches a selector item against children of an element.
*
* @param selectorItem Selector item.
* @param element Element.
* @returns Result.
*/
matchChildOfElement(selectorItem, element) {
for (const child of element[PropertySymbol.elementArray]) {
const match = selectorItem.match(child);
if (match) {
return match;
}
const childMatch = this.matchChildOfElement(selectorItem, child);
if (childMatch) {
return childMatch;
}
}
}
/**
* Returns the selector string.
*
* @returns Selector string.
*/
getSelectorString() {
return `${this.tagName ? this.tagName.toLowerCase() : ''}${this.id ? `#${this.id}` : ''}${this.classNames ? `.${this.classNames.join('.')}` : ''}${this.attributes
? this.attributes
.map((attribute) => `[${attribute.name}${attribute.value ? `${attribute.operator || ''}="${attribute.value}"` : ''}]`)
.join('')
: ''}${this.pseudos
? this.pseudos
.map((pseudo) => `:${pseudo.name}${pseudo.arguments ? `(${pseudo.arguments})` : ''}`)
.join('')
: ''}`;
}
}
exports.default = SelectorItem;
//# sourceMappingURL=SelectorItem.cjs.map