mvdom
Version:
deprecated - Moved to dom-native package
431 lines (373 loc) • 15.9 kB
text/typescript
import { asNodeArray } from './utils';
export type AppendPosition = "first" | "last" | "empty" | "before" | "after";
// --------- DOM Query Shortcuts --------- //
// Shortcut for .querySelector
// return the first element matching the selector from this el (or document if el is not given)
/** Shortchut to el.querySelector, but allow el to be null (in which case will return null) */
export function first(el: Document | HTMLElement | DocumentFragment | null | undefined, selector: string): HTMLElement | null;
export function first(selector: string): HTMLElement | null;
export function first(el: Document | HTMLElement | DocumentFragment | null | undefined): HTMLElement | null;
export function first(el_or_selector: Document | HTMLElement | DocumentFragment | string | null | undefined, selector?: string) {
// We do not have a selector at all, then, this call is for firstElementChild
if (!selector && typeof el_or_selector !== "string") {
const el = el_or_selector as HTMLElement | DocumentFragment;
// try to get
const firstElementChild = el.firstElementChild;
// if firstElementChild is null/undefined, but we have a firstChild, it is perhaps because not supported
if (!firstElementChild && el.firstChild) {
// If the firstChild is of type Element, return it.
if (el.firstChild.nodeType === 1) {
return el.firstChild;
}
// Otherwise, try to find the next element (using the next)
else {
// TODO: Needs to look at typing here, this is a ChildNode
return next(el.firstChild);
}
}
return firstElementChild as HTMLElement;
}
// otherwise, the call was either (selector) or (el, selector), so foward to the querySelector
else {
return _execQuerySelector(false, el_or_selector, selector);
}
}
// TODO: might need to return readonly HTMLElement[] to be consistent with asNodeArray
/** Convenient and normalized API for .querySelectorAll. Return Array (and not node list) */
export function all(el: Document | HTMLElement | DocumentFragment | null | undefined, selector: string): HTMLElement[];
export function all(selector: string): HTMLElement[];
export function all(el: Document | HTMLElement | DocumentFragment | null | undefined | string, selector?: string) {
const nodeList = _execQuerySelector(true, el, selector);
return (nodeList != null) ? asNodeArray(nodeList) : [];
}
/**
* Get the eventual next sibling of an HTMLElement given (optionally as selector)
*/
export function next(el: Node | null | undefined, selector?: string): HTMLElement | null {
return _sibling(true, el, selector) as HTMLElement; // assume HTMLElement
}
/**
* Get the eventual previous sibling
*/
export function prev(el: Node | null | undefined, selector?: string): HTMLElement | null {
return _sibling(false, el, selector) as HTMLElement; // assume HTMLElement
}
// By default use the document.closest (if not implemented, use the matches to mimic the logic)
// return null if not found
export function closest(el: HTMLElement | null | undefined, selector: string): HTMLElement | null {
return (el) ? el.closest(selector) as HTMLElement | null : null;
}
// --------- /DOM Query Shortcuts --------- //
//#region ---------- DOM Manipulation ----------
export function append<T extends HTMLElement | HTMLElement[] | DocumentFragment | string>(this: any, refEl: HTMLElement | DocumentFragment, newEl: T, position?: AppendPosition): T extends HTMLElement ? HTMLElement : HTMLElement[];
export function append(this: any, refEl: HTMLElement | DocumentFragment, newEl: HTMLElement | HTMLElement[] | DocumentFragment | string, position?: AppendPosition): HTMLElement | HTMLElement[] {
let parentEl: HTMLElement | DocumentFragment;
let nextSibling: HTMLElement | null = null;
let result: HTMLElement | HTMLElement[];
// make newEl a document fragment if string passed
if (typeof newEl === 'string') {
newEl = frag(newEl);
}
// NOTE: need to do it before we append in the case for DocumentFragment case.
// NOTE: we assume HTML element as per MVDOM current approach.
if (newEl instanceof Array) {
result = newEl;
// Create a document frag
const fragment = document.createDocumentFragment();
for (const elItem of newEl) {
fragment.appendChild(elItem);
}
newEl = fragment;
} else if (newEl instanceof DocumentFragment) {
result = [...newEl.children] as HTMLElement[]; // take the liberty to assume HTMLElememt
} else {
result = newEl;
}
// default is "last"
position = (position) ? position : "last";
//// 1) We determine the parentEl
if (position === "last" || position === "first" || position === "empty") {
parentEl = refEl;
} else if (position === "before" || position === "after") {
parentEl = refEl.parentNode as HTMLElement;
if (!parentEl) {
throw new Error("mvdom ERROR - The referenceElement " + refEl + " does not have a parentNode. Cannot insert " + position);
}
}
//// 2) We determine if we have a nextSibling or not
// if "first", we try to see if there is a first child
if (position === "first") {
nextSibling = first(refEl); // if this is null, then, it will just do an appendChild
// Note: this might be a text node but this is fine in this context.
}
// if "before", then, the refEl is the nextSibling
else if (position === "before") {
nextSibling = refEl as HTMLElement;
}
// if "after", try to find the next Sibling (if not found, it will be just a appendChild to add last)
else if (position === "after") {
nextSibling = next(refEl);
}
//// 3) We append the newEl
// if we have a next sibling, we insert it before
if (nextSibling) {
parentEl!.insertBefore(newEl, nextSibling);
}
// otherwise, we just do a append last
else {
if (position === "empty") {
// NOTE: the assumption here is that innerHTML will go faster than iterating through the lastChild, but for DocumentFragment, no choice
if (parentEl! instanceof HTMLElement) {
parentEl.innerHTML = '';
} else if (parentEl! instanceof DocumentFragment) {
while (parentEl.lastChild) {
parentEl.removeChild(parentEl.lastChild);
}
}
}
parentEl!.appendChild(newEl);
}
return result;
}
/**
* Returns a DocumentFragment for the html string. If html is null or undefined, returns an empty document fragment.
* @param html the html string or null/undefined
*/
export function frag(html: string | null | undefined) {
// make it null proof
html = (html) ? html.trim() : null;
const template = document.createElement("template");
if (html) {
template.innerHTML = html;
}
return template.content;
}
//#endregion ---------- /DOM Manipulation ----------
//#region ---------- style ----------
/** Conditional typing override for */
export function style<T extends HTMLElement | HTMLElement[] | null | undefined>(el: T, style: Partial<CSSStyleDeclaration>): T;
// NOTE: If the implementation style... does not return 'T | null' then, the `return null;` says that does not match T (the guard seems to not work).
// The trick is to override the definition with above, and it work.
export function style<T extends HTMLElement | HTMLElement[] | null>(el: T, style: Partial<CSSStyleDeclaration>): T {
if (el == null) return el;
// TODO: Would be nice to make this more typed, however function constraints and assignment below matches.
if (el instanceof HTMLElement) {
_styleEl(el, style);
} else if (el instanceof Array) {
for (const elItem of el) {
_styleEl(elItem, style);
}
}
return el;
}
function _styleEl(el: HTMLElement, style: Partial<CSSStyleDeclaration>) {
for (const name of Object.keys(style)) {
(<any>el.style)[name] = (<any>style)[name];
}
}
//#endregion ---------- /style ----------
//#region ---------- className ----------
/**
* Minimilist DOM css class name helper. Add or Remove class name based on object property value.
*
* e.g., `className(el, {prime: true, 'dark-mode': false} )`
*
* - false | null means remove class name
* - true | any defined object add class name
* - undefined values will ignore the property name
*
* @returns pathrough return
*
* Examples:
* - `className(el, {prime: true, 'dark-mode': false} )` add css class 'prime' and remove 'dark-mode'
* - `className(el, {prime: someNonNullObject, 'dark-mode': false})` same as above.
* - `className(els, {prime: someNonNullObject, 'dark-mode': false})` Will add/remove class for all of the elements.
*
* @param el
* @param keyValues e.g. `{prime: true, 'dark-mode': fase, 'compact-view': someObj}`
*/
export function className<E extends HTMLElement | HTMLElement[] | null | undefined>(els: E, keyValues: { [name: string]: boolean | object | null | undefined }): E {
if (els instanceof Array) {
for (const el of els) {
_setClassName(el, keyValues);
}
}
else {
_setClassName(els as HTMLElement, keyValues);
}
return els;
}
function _setClassName(el: HTMLElement, keyValues: { [name: string]: boolean | object | null | undefined }) {
for (const name of Object.keys(keyValues)) {
const val = keyValues[name];
if (val === null || val === false) {
el.classList.remove(name);
} else if (val !== undefined) { // for now, do nothing if undefined
el.classList.add(name);
}
}
}
//#endregion ---------- /className ----------
//#region ---------- attr ----------
// conditional typing
type Val = string | null;
type NameValMap = { [name: string]: string | null | boolean };
/**
* setAttribute DOM helper to Get and Set attribute to DOM HTMLElement(s).
*
* Note: For setters, null and boolean-false value will remove the attribute, `true` will set empty string.
*
* Examples:
*
* Getters:
* - `attr(el, 'name')` returns `string | null`, Get of the attribute `name`
* - `attr(el, ['name', 'label'])` returns the attribute `[name, label]` (string | null)[]
* - `attr(els,'name')` returns `[name, name, ...]` for each attribute for all els. Item is null if no attribute with this anme.
* - `attr(els,['name', 'label'])` returns `[name,label][]` for each element.
*
* Setters:
* - `attr(el, 'name', 'username')` Set attribute name. If value is null, then, remove will be applied. TODO: Might deprecate. But ok shorthand, and handle the null/remove case, and return el.
* - `attr(el, {name: 'username', placeholder: 'Enter username'})` Will set the attributes specified in the object to this element, and returl el,
* - `attr(els, {checked: true, readonly: ''})` Will set the attributes specified in the object for all of the elements, and return els.
*
* TODO: On 'set' should be a passtrough return (return null | undefined as well)
*/
export function attr(el: HTMLElement, name: string): string | null;
export function attr(els: HTMLElement[], name: string): (string | null)[];
export function attr(el: HTMLElement, names: string[]): (string | null)[];
export function attr(els: HTMLElement[], names: string[]): (string | null)[][];
export function attr(el: HTMLElement, nameValues: { [name: string]: string | null | boolean }): HTMLElement;
export function attr(els: HTMLElement[], nameValues: { [name: string]: string | null | boolean }): HTMLElement[];
export function attr(el: HTMLElement, name: string, val: string | null | boolean): HTMLElement;
export function attr(els: HTMLElement[], name: string, val: string | null | boolean): HTMLElement[];
// implementation
export function attr<E extends HTMLElement | HTMLElement[], A extends string | string[] | NameValMap>(els: E, arg: A, val?: string | null | boolean): Val | Val[] | Val[][] | E {
// if we have a val, then, its a single attribute setting (on one or more element)
if (val !== undefined) {
if (typeof arg !== 'string') {
throw new Error(`attr - attr(els, name, value) must have name as string and not: ${arg}`);
}
const name = arg as string;
if (els instanceof Array) {
for (const el of els) {
_setAttribute(el, name, val);
}
} else {
_setAttribute(els as HTMLElement, name, val);
}
return els;
}
// else, if arg is string or array, we assume its a getter (for now, assume the array is an array of string)
else if (typeof arg === 'string' || arg instanceof Array) {
return _attrGet(els, arg as (string | string[]));
}
// otherwise, it is a setter
else {
return _attrSet(els, arg as NameValMap); // TODO
}
}
export function _attrSet<E extends HTMLElement | HTMLElement[]>(els: E, arg: NameValMap): E {
if (els instanceof Array) {
for (const el of els) {
_setAttributes(el, arg);
}
} else {
_setAttributes(els as HTMLElement, arg);
}
return els;
}
function _setAttributes(el: HTMLElement, nameValueObject: NameValMap) {
for (const name of Object.keys(nameValueObject)) {
_setAttribute(el, name, nameValueObject[name]);
}
}
function _setAttribute(el: HTMLElement, name: string, val: string | null | boolean) {
// if it is a boolean, true will set the attribute empty, and false will set txtVal to null, which will remove it.
const txtVal = (typeof val !== 'boolean') ? val : (val === true) ? '' : null;
if (txtVal !== null) {
el.setAttribute(name, txtVal);
} else {
el.removeAttribute(name);
}
}
export function _attrGet<E extends HTMLElement | HTMLElement[], A extends string | string[]>(els: E, arg: A): Val | Val[] | Val[][] | E {
// If HTMLElement[]
if (els instanceof Array) {
const ells = els as HTMLElement[];
return ells.map(el => {
const r = _getAttrEl(el as HTMLElement, arg as string);
return r;
});
}
// otherwise, assum HTMLElement
else {
const r = _getAttrEl(els as HTMLElement, arg);
return r;
}
}
export function _getAttrEl<N extends string | string[]>(el: HTMLElement, names: N):
N extends string ? string | null : (string | null)[];
export function _getAttrEl(el: HTMLElement, names: string | string[]): any | (string | null) | (string | null)[] {
if (names instanceof Array) {
return names.map(n => { return el.getAttribute(n) });
}
// else singloe
else {
return el.getAttribute(names);
}
}
//#endregion ---------- /attr ----------
//#region ---------- elem ----------
/**
* Shorthand for document.createElement(name)
* @param name tag name
*/
export function elem(name: string): HTMLElement;
/**
* Create multiple HTMLElement via document.createElement
* @param names tag names
*/
export function elem(...names: string[]): HTMLElement[];
export function elem(...names: string[]): HTMLElement | HTMLElement[] {
if (names.length === 1) {
return document.createElement(names[0]);
} else {
return names.map(n => { return document.createElement(n) });
}
}
//#endregion ---------- /elem ----------
/**
* Return the next or previous Element sibling
* @param next
* @param el
* @param selector
*/
function _sibling(next: boolean, el: Node | undefined | null, selector?: string) {
const sibling: 'nextSibling' | 'previousSibling' = (next) ? 'nextSibling' : 'previousSibling';
let tmpEl = (el) ? el[sibling] : null;
// use "!=" for null and undefined
while (tmpEl != null && (<any>tmpEl) != document) {
// only if node type is of Element, otherwise,
if (tmpEl.nodeType === 1 && (!selector || (<Element>tmpEl).matches(selector))) {
return tmpEl as Element;
}
tmpEl = tmpEl[sibling];
}
return null;
}
// util: querySelector[All] wrapper
function _execQuerySelector(all: boolean, elOrSelector?: Document | HTMLElement | DocumentFragment | null | string, selector?: string) {
let el: HTMLElement | Document | DocumentFragment | null = null;
// if el is null or undefined, means we return nothing.
if (elOrSelector == null) {
return null;
}
// if selector is undefined, it means we select from document and el is the document
if (typeof selector === "undefined") {
selector = elOrSelector as string;
el = document;
} else {
el = elOrSelector as HTMLElement | DocumentFragment;
}
return (all) ? el.querySelectorAll(selector) : el.querySelector(selector);
}