import { InvalidArgumentError } from './errors' import { isString } from './utils' interface ClassOptions { [s: string]: boolean } interface AttributeOptions { [s: string]: string | number | boolean | undefined } interface PropertyOptions { [s: string]: any } interface ElementOptions { type?: string class?: string | ClassOptions | undefined attr?: AttributeOptions | undefined prop?: PropertyOptions | undefined children?: Array | undefined api?: T } class Selection { /** Creates a new selection with the element provided */ constructor(public element: HTMLElement) { } /** Returns true if the value passed in is a Selection */ public static isSelection (value: any): value is Selection { return value instanceof Selection } /** Add a selected element, or array of selected elements to this selection */ public add (selection: Selection | Array): this { if (Array.isArray(selection)) { for (const s of selection) { this.element.appendChild(s.element) } } else { this.element.appendChild(selection.element) } return this } /** Removes children from this selection */ public remove (selection: Selection | Array): this { if (Array.isArray(selection)) { for (const s of selection) { this.element.removeChild(s.element) } } else { this.element.removeChild(selection.element) } return this } /** Sets the child elements of this selection */ public set (selection: Selection | Array): this { this.removeAll() if (Array.isArray(selection)) { for (const s of selection) { this.element.appendChild(s.element) } } else { this.element.appendChild(selection.element) } return this } /** Removes all children elements from this element */ public removeAll (): this { while (this.element.firstChild) { this.element.removeChild(this.element.firstChild) } return this } /** Detaches this element from its parent */ public removeSelf (): this { if (this.element.parentElement) { this.element.parentElement.removeChild(this.element) } return this } /** Gets the text of the selected element */ public text (): string /** Sets the text of ths selected element */ public text (text: string): this public text (text?: string): this | string { if (arguments.length === 0) { return this.element.innerText } else { this.element.innerText = text return this } } /** Gets the class for the element */ public class (): string /** Sets the class for the element */ public class (cls: string | ClassOptions): this public class (cls?: string | ClassOptions): this | string { if (arguments.length > 0) { if (typeof cls === 'string') { this.element.className = cls } else { for (const key of Object.keys(cls)) { if (cls[key]) { this.element.classList.add(key) } else { this.element.classList.remove(key) } } } return this } else { return this.element.className } } /** Returns true if the selected element has the class */ public hasClass (cls: string): boolean { return this.element.classList.contains(cls) } /** Gets an attribute on the element */ public attr (key: string): string | undefined /** Sets one or more attributes on the element */ public attr (options: AttributeOptions): this /** Sets an attribute on the element */ public attr (key: string, value: string | number | boolean | undefined): this public attr (keyOrOptions: string | AttributeOptions, value?: string | number | boolean | undefined): this | string | undefined { if (arguments.length === 2) { if (typeof keyOrOptions === 'string') { if (value !== undefined) { this.element.setAttribute(keyOrOptions, value.toString()) } else { this.element.removeAttribute(keyOrOptions) } } else { throw new InvalidArgumentError("key should be a string") } } else if (arguments.length === 1) { if (typeof keyOrOptions === 'string') { return this.element.getAttribute(keyOrOptions) || undefined } else { for (const key of Object.keys(keyOrOptions)) { const value = keyOrOptions[key] if (value !== undefined) { this.element.setAttribute(key, value.toString()) } else { this.element.removeAttribute(key) } } } } else { throw new InvalidArgumentError("expected 1 or 2 arguments") } return this } /** Gets a property of the element */ public prop (key: string): any /** Sets one or more properties of the element */ public prop (options: PropertyOptions): this /** Sets a property of the element */ public prop (key: string, value: any | undefined): this public prop (keyOrOptions: string | PropertyOptions, value?: any | undefined): this | any { if (arguments.length === 2) { if (typeof keyOrOptions === 'string') { (this.element as any)[keyOrOptions] = value } else { throw new InvalidArgumentError("key should be a string") } } else if (arguments.length === 1) { if (typeof keyOrOptions === 'string') { return (this.element as any)[keyOrOptions] } else { for (const key of Object.keys(keyOrOptions)) { (this.element as any)[key] = keyOrOptions[key] } } } else { throw new InvalidArgumentError("expected 1 or 2 arguments") } return this } /** Gets the api attached to this element */ public api (): T | undefined /** Sets the api attached to this element */ public api (api: T): this /** Gets the api attached to this element */ public api (name: string): T | undefined /** Sets the api attached to this element */ public api (name: string, api: T): this public api (nameOrApi?: string | T, api?: T): T | this | undefined { const elementAsAny = (this.element as any) const name = isString(nameOrApi) ? nameOrApi : 'default' const apiToSet = api ? api : !isString(nameOrApi) ? nameOrApi : undefined if (apiToSet) { elementAsAny.__charge__ = elementAsAny.__charge__ || {} elementAsAny.__charge__.api = elementAsAny.__charge__.api || {} elementAsAny.__charge__.api[name] = apiToSet return this } else { return elementAsAny.__charge__ && elementAsAny.__charge__.api ? elementAsAny.__charge__.api[name] : undefined } } /** Listens for events on the selected element */ public on (name: string | Array, handler: (evt: Event) => void): this { if (Array.isArray(name)) { for (const n of name) { this.element.addEventListener(n, handler) } } else { this.element.addEventListener(name, handler) } return this } /** Removes an event listener from the selected element */ public off (name: string | Array, handler?: (evt: Event) => void) { if (Array.isArray(name)) { for (const n of name) { this.element.removeEventListener(n, handler) } } else { this.element.removeEventListener(name, handler) } return this } } function createSelectionFromElement (element: Element): Selection { if (element instanceof HTMLElement) { return new Selection(element) } else { throw new Error(typeof (element) + ' elements are not supported yet in the Selection api') } } /** Selects an element in the document (using document.querySelector under the hood) * An element can be converted into a selection using this method */ function select (selector: string | HTMLElement | Selection): Selection { if (typeof selector === 'string') { const element = document.querySelector(selector) return createSelectionFromElement(element) } else if (selector instanceof Element) { return createSelectionFromElement(selector) } else if (selector instanceof Selection) { return selector } else { throw new Error('Invalid value passed into select(): ' + selector) } } /** Selects all elements from the document (using document.querySelectorAll under the hood) */ function selectAll (selector: string): Array { if (typeof selector === 'string') { const elements = document.querySelectorAll(selector) return Array.from(elements).map(createSelectionFromElement) } else { throw new Error('Invalid value passed into selectAll(): ' + selector) } } /** Creates a new detached element */ function element (options?: ElementOptions): Selection { const element = document.createElement(options ? options.type || 'div' : 'div') const selection = new Selection(element) if (options) { if (options.class) { selection.class(options.class) } if (options.attr) { selection.attr(options.attr) } if (options.prop) { selection.prop(options.prop) } if (options.children) { selection.set(options.children) } if (options.api) { selection.api(options.api) } } return selection } export { Selection, select, selectAll, element }