* // 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) => {
const grandParent = parent.parentNode;
if (!grandParent) return;
while (parent.firstChild) {
grandParent.insertBefore(parent.firstChild, parent);
}
parent.remove();
});
return this;
}
/** Replace each element with provided content. */
replaceWith(content: string | Element): BQueryCollection {
const replacements: Element[] = [];
this.elements.forEach((el, index) => {
const replacement =
typeof content === 'string'
? createElementFromHtml(content)
: index === 0
? content
: (content.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;
}
return Array.from(first.parentElement.children).indexOf(first);
}
/**
* 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 {
const key = `${event}:${selector}`;
applyAll(this.elements, (el) => {
const wrapper: EventListener = (e: Event) => {
const target = (e.target as Element).closest(selector);
if (target && el.contains(target)) {
handler(e, target);
}
};
// Get or create the handler maps for this element
if (!this.delegatedHandlers.has(el)) {
this.delegatedHandlers.set(el, new Map());
}
const elementHandlers = this.delegatedHandlers.get(el)!;
if (!elementHandlers.has(key)) {
elementHandlers.set(key, new Map());
}
elementHandlers.get(key)!.set(handler, wrapper);
el.addEventListener(event, wrapper);
});
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 {
const key = `${event}:${selector}`;
applyAll(this.elements, (el) => {
const elementHandlers = this.delegatedHandlers.get(el);
if (!elementHandlers) return;
const handlers = elementHandlers.get(key);
if (!handlers) return;
const wrapper = handlers.get(handler);
if (wrapper) {
el.removeEventListener(event, wrapper);
handlers.delete(handler);
// Clean up empty maps
if (handlers.size === 0) {
elementHandlers.delete(key);
}
if (elementHandlers.size === 0) {
this.delegatedHandlers.delete(el);
}
}
});
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) {
for (const child of Array.from(el.children)) {
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);
const seen = new Set();
const results: Element[] = [];
for (const el of this.elements) {
const parent = el.parentElement;
if (!parent) continue;
for (const sibling of Array.from(parent.children)) {
if (!selfSet.has(sibling) && !seen.has(sibling)) {
seen.add(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 {
applyAll(this.elements, (el) => {
el.innerHTML = '';
});
return this;
}
/** @internal */
private insertAll(content: InsertableContent, position: InsertPosition): void {
if (typeof content === 'string') {
// Sanitize once and reuse for all elements
const sanitized = sanitizeContent(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);
});
}
}