* // Result:
AB
* ```
*/
unwrap(): this {
// Collect unique parent elements to avoid removing the same parent multiple times.
const parents = new Set
();
for (const el of this.elements) {
if (el.parentElement) {
parents.add(el.parentElement);
}
}
// Unwrap each parent once: move all children out, then remove the wrapper.
parents.forEach((parent) => {
if (!parent.parentNode) return;
// Single tree mutation: moves all children out and removes the wrapper.
parent.replaceWith(...parent.childNodes);
});
return this;
}
/** Replace each element with provided content. */
replaceWith(content: string | Element): BQueryCollection {
// Sanitize and parse string content once; clone for subsequent elements.
const proto = typeof content === 'string' ? createElementFromHtml(content) : content;
const replacements: Element[] = [];
this.elements.forEach((el, index) => {
const replacement = index === 0 ? proto : (proto.cloneNode(true) as Element);
el.replaceWith(replacement);
replacements.push(replacement);
});
return new BQueryCollection(replacements);
}
/**
* Removes all elements from the DOM while keeping the wrapped nodes available
* for later reuse.
*
* @returns The instance for method chaining
*/
detach(): this {
return this.remove();
}
/**
* Gets the zero-based sibling index of the first element in the collection.
*
* @returns Index of the first element, or -1 when unavailable
*/
index(): number {
const first = this.first();
if (!first?.parentElement) {
return -1;
}
let index = 0;
for (let node = first.previousElementSibling; node; node = node.previousElementSibling) {
index++;
}
return index;
}
/**
* Returns the child nodes of the first element, including text nodes and comments.
*
* @returns Array of child nodes from the first element
*/
contents(): ChildNode[] {
return Array.from(this.first()?.childNodes ?? []);
}
/**
* Gets the offset parent of the first element in the collection.
*
* @returns Offset parent element, or null when unavailable
*/
offsetParent(): Element | null {
const first = this.first();
return isHTMLElement(first) ? first.offsetParent : null;
}
/**
* Gets the position of the first element relative to its offset parent.
*
* @returns Position object with top and left coordinates
*/
position(): { top: number; left: number } {
const first = this.first();
if (!isHTMLElement(first)) {
return { top: 0, left: 0 };
}
return {
top: first.offsetTop,
left: first.offsetLeft,
};
}
/**
* Gets the inner width of the first element (content + padding, excluding border).
*
* @returns Inner width in pixels, or 0 when the collection is empty
*/
innerWidth(): number {
return getInnerSize(this.first(), 'width');
}
/**
* Gets the inner height of the first element (content + padding, excluding border).
*
* @returns Inner height in pixels, or 0 when the collection is empty
*/
innerHeight(): number {
return getInnerSize(this.first(), 'height');
}
/**
* Gets the outer width of the first element, optionally including margins.
*
* @param includeMargin - When true, include horizontal margins
* @returns Outer width in pixels
*/
outerWidth(includeMargin: boolean = false): number {
return getOuterSize(this.first(), 'width', includeMargin);
}
/**
* Gets the outer height of the first element, optionally including margins.
*
* @param includeMargin - When true, include vertical margins
* @returns Outer height in pixels
*/
outerHeight(includeMargin: boolean = false): number {
return getOuterSize(this.first(), 'height', includeMargin);
}
/**
* Shows all elements.
*
* @param display - Optional display value (default: '')
* @returns The instance for method chaining
*/
show(display: string = ''): this {
applyAll(this.elements, (el) => {
el.removeAttribute('hidden');
(el as HTMLElement).style.display = display;
});
return this;
}
/**
* Hides all elements.
*
* @returns The instance for method chaining
*/
hide(): this {
applyAll(this.elements, (el) => {
(el as HTMLElement).style.display = 'none';
});
return this;
}
/**
* Adds an event listener to all elements.
*
* @param event - Event type
* @param handler - Event handler
* @returns The instance for method chaining
*/
on(event: string, handler: EventListenerOrEventListenerObject): this {
applyAll(this.elements, (el) => el.addEventListener(event, handler));
return this;
}
/**
* Adds a one-time event listener to all elements.
*
* @param event - Event type
* @param handler - Event handler
* @returns The instance for method chaining
*/
once(event: string, handler: EventListener): this {
applyAll(this.elements, (el) => el.addEventListener(event, handler, { once: true }));
return this;
}
/**
* Removes an event listener from all elements.
*
* @param event - Event type
* @param handler - The handler to remove
* @returns The instance for method chaining
*/
off(event: string, handler: EventListenerOrEventListenerObject): this {
applyAll(this.elements, (el) => el.removeEventListener(event, handler));
return this;
}
/**
* Triggers a custom event on all elements.
*
* @param event - Event type
* @param detail - Optional event detail
* @returns The instance for method chaining
*/
trigger(event: string, detail?: unknown): this {
applyAll(this.elements, (el) => {
el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
});
return this;
}
/**
* Adds a delegated event listener to all elements.
* Events are delegated to matching descendants.
*
* Use `undelegate()` to remove the listener later.
*
* @param event - Event type to listen for
* @param selector - CSS selector to match against event targets
* @param handler - Event handler function
* @returns The instance for method chaining
*
* @example
* ```ts
* const handler = (e, target) => console.log('Clicked:', target.textContent);
* $$('.container').delegate('click', '.item', handler);
*
* // Later, remove the delegated listener:
* $$('.container').undelegate('click', '.item', handler);
* ```
*/
delegate(
event: string,
selector: string,
handler: (event: Event, target: Element) => void
): this {
applyAll(this.elements, (el) => addDelegatedListener(el, event, selector, handler));
return this;
}
/**
* Removes a delegated event listener previously added with `delegate()`.
*
* @param event - Event type that was registered
* @param selector - CSS selector that was used
* @param handler - The original handler function passed to delegate()
* @returns The instance for method chaining
*
* @example
* ```ts
* const handler = (e, target) => console.log('Clicked:', target.textContent);
* $$('.container').delegate('click', '.item', handler);
*
* // Remove the delegated listener:
* $$('.container').undelegate('click', '.item', handler);
* ```
*/
undelegate(
event: string,
selector: string,
handler: (event: Event, target: Element) => void
): this {
applyAll(this.elements, (el) => removeDelegatedListener(el, event, selector, handler));
return this;
}
/**
* Finds all descendant elements matching the selector across all elements
* in the collection. Returns a new BQueryCollection with the results.
*
* @param selector - CSS selector to match
* @returns A new BQueryCollection with all matching descendants
*
* @example
* ```ts
* $$('.container').find('.item').addClass('highlight');
* ```
*/
find(selector: string): BQueryCollection {
const seen = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const found = el.querySelectorAll(selector);
for (let i = 0; i < found.length; i++) {
if (!seen.has(found[i])) {
seen.add(found[i]);
results.push(found[i]);
}
}
}
return new BQueryCollection(results);
}
/**
* Gets the closest element or ancestor matching a selector for each element in
* the collection, including the element itself. Duplicates are removed from the
* result.
*
* @param selector - CSS selector to match
* @returns A new BQueryCollection with matching elements or ancestors
*
* @example
* ```ts
* $$('.item').closest('.container');
* ```
*/
closest(selector: string): BQueryCollection {
const seen = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const match = el.closest(selector);
if (match && !seen.has(match)) {
seen.add(match);
results.push(match);
}
}
return new BQueryCollection(results);
}
/**
* Gets the parent element of each element in the collection.
* Duplicates are removed (e.g. siblings sharing a parent).
*
* @returns A new BQueryCollection with unique parent elements
*
* @example
* ```ts
* $$('.item').parent().addClass('has-items');
* ```
*/
parent(): BQueryCollection {
const seen = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const p = el.parentElement;
if (p && !seen.has(p)) {
seen.add(p);
results.push(p);
}
}
return new BQueryCollection(results);
}
/**
* Gets the direct children of every element in the collection.
* Duplicates are removed from the result.
*
* @returns A new BQueryCollection with child elements
*
* @example
* ```ts
* $$('.list').children().addClass('child');
* ```
*/
children(): BQueryCollection {
const seen = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const kids = el.children;
for (let i = 0; i < kids.length; i++) {
const child = kids[i];
if (!seen.has(child)) {
seen.add(child);
results.push(child);
}
}
}
return new BQueryCollection(results);
}
/**
* Gets all siblings of every element in the collection (excluding the
* elements themselves). Duplicates are removed.
*
* @returns A new BQueryCollection with sibling elements
*
* @example
* ```ts
* $$('.active').siblings().removeClass('active');
* ```
*/
siblings(): BQueryCollection {
const selfSet = new Set(this.elements);
// Elements sharing a parent yield the same sibling set, so scan each
// parent's children only once.
const visitedParents = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const parent = el.parentElement;
if (!parent || visitedParents.has(parent)) continue;
visitedParents.add(parent);
const kids = parent.children;
for (let i = 0; i < kids.length; i++) {
const sibling = kids[i];
if (!selfSet.has(sibling)) {
results.push(sibling);
}
}
}
return new BQueryCollection(results);
}
/**
* Gets the next sibling element of each element in the collection.
* Elements without a next sibling are skipped.
*
* @returns A new BQueryCollection with next sibling elements
*
* @example
* ```ts
* $$('.current').next().addClass('upcoming');
* ```
*/
next(): BQueryCollection {
const seen = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const n = el.nextElementSibling;
if (n && !seen.has(n)) {
seen.add(n);
results.push(n);
}
}
return new BQueryCollection(results);
}
/**
* Gets the previous sibling element of each element in the collection.
* Elements without a previous sibling are skipped.
*
* @returns A new BQueryCollection with previous sibling elements
*
* @example
* ```ts
* $$('.current').prev().addClass('previous');
* ```
*/
prev(): BQueryCollection {
const seen = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const p = el.previousElementSibling;
if (p && !seen.has(p)) {
seen.add(p);
results.push(p);
}
}
return new BQueryCollection(results);
}
/**
* Removes all elements from the DOM.
*
* @returns The instance for method chaining
*/
remove(): this {
applyAll(this.elements, (el) => el.remove());
return this;
}
/**
* Clears all child nodes from all elements.
*
* @returns The instance for method chaining
*/
empty(): this {
// replaceChildren skips the HTML parser and needs no Trusted Types sink.
applyAll(this.elements, (el) => el.replaceChildren());
return this;
}
/** @internal */
private insertAll(content: InsertableContent, position: InsertPosition): void {
if (typeof content === 'string') {
// Sanitize once and reuse for all elements
const sanitized = trustedHtmlForSink(content);
applyAll(this.elements, (el) => {
el.insertAdjacentHTML(position, sanitized);
});
return;
}
const elements = toElementList(content);
this.elements.forEach((el, index) => {
const nodes =
index === 0 ? elements : elements.map((node) => node.cloneNode(true) as Element);
insertContent(el, nodes, position);
});
}
}