import {$} from 'select-dom';
import {setFetch} from 'push-form';
// Nodes may be exactly `null`
import {type Nullable} from 'vitest';
// `content.fetch` is Firefox’s way to make fetches from the page instead of from a different context
// This will set the correct `origin` header without having to use XMLHttpRequest
// https://stackoverflow.com/questions/47356375/firefox-fetch-api-how-to-omit-the-origin-header-in-the-request
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#XHR_and_Fetch
if (window.content?.fetch) {
setFetch(window.content.fetch);
}
/**
* Append to an element, but before a element that might not exist.
* @param parent Element (or its selector) to which append the `child`
* @param before Selector of the element that `child` should be inserted before
* @param child Element to append
* @example
*
*
*
*
*
*
*
* appendBefore('parent', 'nope', );
*
*
*
*
*
*
*
*/
export const appendBefore = (parent: string | Element, before: string, child: Element): void => {
if (typeof parent === 'string') {
parent = $(parent)!;
}
// Select direct children only
const beforeElement = $(`:scope > :is(${before})`, parent);
if (beforeElement) {
beforeElement.before(child);
} else {
parent.append(child);
}
};
export const wrap = (target: Element | ChildNode, wrapper: Element): void => {
target.before(wrapper);
wrapper.append(target);
};
export const wrapAll = (targets: Iterable, wrapper: Wrapper): Wrapper => {
const [first, ...rest] = targets;
first.before(wrapper);
wrapper.append(first, ...rest);
return wrapper;
};
export const isEditable = (node: unknown): boolean => node instanceof HTMLTextAreaElement
|| node instanceof HTMLInputElement
|| (node instanceof HTMLElement && node.isContentEditable);
export const frame = async (): Promise => new Promise(resolve => {
requestAnimationFrame(resolve);
});
export const highlightTab = (tabElement: Element): void => {
tabElement.classList.add('selected');
tabElement.setAttribute('aria-current', 'page');
};
export const unhighlightTab = (tabElement: Element): void => {
tabElement.classList.remove('selected');
tabElement.removeAttribute('aria-current');
};
const matchString = (matcher: RegExp | string, string: string): boolean =>
typeof matcher === 'string' ? matcher === string : matcher.test(string);
const escapeMatcher = (matcher: RegExp | string): string =>
typeof matcher === 'string' ? `"${matcher}"` : String(matcher);
const isTextNode = (node: Text | ChildNode): boolean =>
node instanceof Text || ([...node.childNodes].every(childNode => childNode instanceof Text));
export const isTextNodeContaining = (node: Nullable, expectation: RegExp | string): boolean => {
// Make sure only text is being considered, not links, icons, etc
if (!node || !isTextNode(node)) {
console.warn('TypeError', node);
throw new TypeError(`Expected Text node, received ${String(node?.nodeName)}`);
}
const content = node.textContent.trim();
return matchString(expectation, content);
};
export const assertNodeContent = (node: Nullable, expectation: RegExp | string): N => {
if (isTextNodeContaining(node, expectation)) {
return node!;
}
console.warn('Error', node!.parentElement);
const content = node!.textContent.trim();
throw new Error(`Expected node matching ${escapeMatcher(expectation)}, found ${escapeMatcher(content)}`);
};
export const removeTextNodeContaining = (node: Text | ChildNode, expectation: RegExp | string): void => {
assertNodeContent(node, expectation);
node.remove();
};