From 89d8fd84793b25a3831f5283036b4cb713b16ab0 Mon Sep 17 00:00:00 2001 From: Oscar Palmer Date: Wed, 7 Aug 2024 07:33:04 +0200 Subject: [PATCH] 0.71.0 - element & html -> @oscarpalmer/toretto --- bun.lockb | Bin 26382 -> 26382 bytes dist/js/element/focusable.js | 144 -------- dist/js/element/index.js | 307 ----------------- dist/js/html/index.js | 124 ------- dist/js/index.js | 489 +--------------------------- package.json | 22 +- src/js/element/attribute.ts | 99 ------ src/js/element/closest.ts | 115 ------- src/js/element/data.ts | 81 ----- src/js/element/find.ts | 137 -------- src/js/element/focusable.ts | 262 --------------- src/js/element/index.ts | 48 --- src/js/element/style.ts | 45 --- src/js/html/index.ts | 99 ------ src/js/html/sanitise.ts | 66 ---- src/js/index.ts | 3 - src/js/internal/colour-property.ts | 18 - src/js/internal/element-value.ts | 37 --- test/element.test.ts | 481 --------------------------- test/html.test.ts | 46 --- test/string.test.ts | 16 + types/element/attribute.d.ts | 26 -- types/element/closest.d.ts | 5 - types/element/data.d.ts | 17 - types/element/find.d.ts | 19 -- types/element/focusable.d.ts | 16 - types/element/index.d.ts | 16 - types/element/style.d.ts | 8 - types/html/index.d.ts | 15 - types/html/sanitise.d.ts | 12 - types/index.d.ts | 3 - types/internal/colour-property.d.ts | 2 - types/internal/element-value.d.ts | 3 - 33 files changed, 19 insertions(+), 2762 deletions(-) delete mode 100644 dist/js/element/focusable.js delete mode 100644 dist/js/element/index.js delete mode 100644 dist/js/html/index.js delete mode 100644 src/js/element/attribute.ts delete mode 100644 src/js/element/closest.ts delete mode 100644 src/js/element/data.ts delete mode 100644 src/js/element/find.ts delete mode 100644 src/js/element/focusable.ts delete mode 100644 src/js/element/index.ts delete mode 100644 src/js/element/style.ts delete mode 100644 src/js/html/index.ts delete mode 100644 src/js/html/sanitise.ts delete mode 100644 src/js/internal/colour-property.ts delete mode 100644 src/js/internal/element-value.ts delete mode 100644 test/element.test.ts delete mode 100644 test/html.test.ts delete mode 100644 types/element/attribute.d.ts delete mode 100644 types/element/closest.d.ts delete mode 100644 types/element/data.d.ts delete mode 100644 types/element/find.d.ts delete mode 100644 types/element/focusable.d.ts delete mode 100644 types/element/index.d.ts delete mode 100644 types/element/style.d.ts delete mode 100644 types/html/index.d.ts delete mode 100644 types/html/sanitise.d.ts delete mode 100644 types/internal/colour-property.d.ts delete mode 100644 types/internal/element-value.d.ts diff --git a/bun.lockb b/bun.lockb index 5e3667b8c1222c7b7585d1cafc1f1c91613b9b3d..9033abfa35215b080a47b7bff5c79387ed269b96 100755 GIT binary patch delta 3387 zcmeHKeNa@_6@T}!yX*s(Pmz5tuzY_AEbJmes=kc^26%vyl&%mlSqK%YsDU^mA%-=L z(Pq?0k9N$q6UBZR(~dQ(OpJ{sW1KQhB&DfMz@)8dGmVMOn3=RC8EyMJ4|cRu|LaWu z>YF+5ch0-#-h0kH_nv#+zHzZ{Torq(Gm)uMe!$DkRAUdHK*8=io_Mk`xGKz!>2AzT$NoH+M*z zB4#?}%zmGS9FO+-kfVVez5dQF40pjaqZe(S>E90I@%)?md;E`ecDG`}xsdz)z3X`P zqn4R*2pB6#gMnRO=0IU!7AGSmDH-xOkSBTr$kvZqXRxcU)4$RGNLS~^^;_3tz%7`K zCk)}m`Zs~>osLf2uiss{32IV!6*{&WqI3nyY0{t+I0q&bibsJdz-NJWU>}eTSqn@8 z)&tp-3Sc5|B?jdQE@Ii(fMmFXrob8?I}`)t?@OVV00bDA4&?bp;?Pbta$jgnJ|yDa z*}Zj*b>o#E9FgAoad2Q@j2<>k%fFOp%B+b9QY~Kbb!xSEJ{6l)#{Z`MNrVHDGEYX4+WY0mJ0Czsft$~ zGEtkN$#0n`sA!6cAo37q8dfSWuNvi*kC>?~N|UF}6pYeDEloyw%^}3Bp4zR8OCXwG-AwL~SQ}}oTAHNnhNLhh6YLKjKp7J!LC6FvUlX7uZGUin*SiQ=b zerVGmd5nJ0l=~H$4{OdTG-ZQFh{JNU$%efx)EWbiq9_=n$*)JzRE#F9q{e#9Um!jY zQEKdB^J+w>-QZ4xd(hzGux%bNxK41*26qZvqrnM;SvR-_aCOYd2VIkvUTrNz|O_qiIN|AtjT@^3kzO8+B#rLZy?CR%h8LDO(o~@@M50zqy1tR~M>bTvdSLHw zstP{+^?}VzAH_9)@!nIT!FTq!mh_I4wEc1V#q(3+-A%v9{P@Jjuh@y6oAUPjN? zI119hac)C)=9`gIv{VJc)nXi3Bz}o=QaW9&Nj7uVv(c>@$5KAVg`gr(9w;A#o0)HA zXε_J{|J1U6t&U_H^j+9F6D;5v(%J5h2&DxxuI6-i7z?~(*Aqlsjky&vKb0xw> zgJC6G%gKo|C0oySA>$iusDStmH_{91aXV*Gwkijd4uVI6hB<5+n~WmDNWts^r(|9h zHkZ?K1}O6mISYI?s08E!v5UM6yd0c%*==49c6A}hSnOF`1Y`#*CHl^N!DQGO2p1Jr zv9{FZal72oN-C}`6RqU0EwlGP8D0bq=LCuhcXo9^tK3!YT1a2kx|D?&3T_9Mlem%raTvEk)4ve2o$Zm}a=4!-cr*{Wl=VtyoxC(B)K7tG3`P+b`|?4Roz9pNIg z<>Bl9{BpnlZ?ahHad}{kw24;MtMYUNeWTtZUog=N^>#OTjY!(lU{47bo!1{}Yk7akXf^aIF*UZ*WF%<~c1O4hZ4=+Svf}TDx7@aEfF5m7 z!{daD(w4WcXFt9H#PHW)~J$s2D*!8bZ;sY(`a{u zE7^(F$A4A#`QZ<-#)u?)T0zSiRZ&k5HmZ(rnc6WY`M$P<(kYW*B@b4ukDhB(7lq4O z=bv^SymT$S^>%x>@cqZgm7o3g)Td`|x15Wn-!I&aP)Xa~+|x_%f$1JZ(&MUJLk~5n>VLNLCdWfpYs*95 zZWs%|cz!RG2v)%{v5 Z{l3|W^y&Aflk@&;8tHI^0{(sGe*qIUhd=-T delta 3470 zcmeHKZBUfg6~6anm)(UduvnC35q1#}L{=7D!65HKM8RF5GKpP;SX~HJEPfy|l@Y@- zsfjkmO3s)FO=4`cNwhOY*HmY0Ga75+s7Yfpv_hhZP0ZM_V@+*Rjic>zUU*A7ALrbUbI)#jUv7I}Zg)GEo?db6uWrwRx|DOd0|nn$w#@206f`{k$#c7_ zFI?O4#>T$dhjLql5Q##B0U9;}W57E?9UWXZB!#eozuotGdXFbc2n$F=5h4!Q+L6Dk zZFQ5lj(YC2ceH-JK?oc80C+60X>F*v1;foTj=~w!amE4Th3E;JPUqKjl2vFX^i^o;)Tn60q-kP_2&*VeM8gbVDli6^47`ca z*pN%WB;Xk!dvXLg4p@dkc>+J&U;~~9LKXfPh#|t;xSqVq6A6FZ-1(@v?V~*}i{HIE zuwldL$Wdjdq@{*i)?*l>9Blz2iOw7HD8Xn}?g(l~RFx`84MtTyLH$Ob{DOvzJ|#z? zlxS6HQm7$Xl_#h_+Na#si1J_*rI=J@T@*E#RC$#8O+I;xhD<(Xx`9&6s$~my(2uzk zx@;_#A5y>Br`U`%Y*v-;8Y#u1TKchfm1tokm!;V91=xdNCJLE~6=yUJTU2FzG^NC- z@-!`q@fnM-b$+^NDpfX`XgEf-oJ2(hx+KwMbTOEzAyzf6!3KVh7Mn^f@1UeYZRQaz z1}xevuxM>I|0o-o`$@18=%Q%1x!Cd+SfSRB?Ya#%W|ta64OTdZop=aM6dp_t(vTI- z#ZpR~s#L^ML!2r%Q-7S#*oB>0LNCUZ8ZV%tTsS@|f)PhV z`!V8<@uksz-$pa79G^}<9-n|4ryWMEr=1D?CV4^+kAtJC)#B)CKStb5kv@tpAhtu~ zK=^k*Y?D;$HkcU=-b+8l=+> zD`hzX(n#%&Agxce(jb_LT&Y3InP{a=sR3!B^I#Xiyb}X5mU<@!X>*#Du7Fv|lNO|s zbSv#h3&?o740aW4PI^Gvs4qQ8JDpbg3TzxzID=H}veI5>Kqk?bV7I^)x&m?n?REv} zwG1m6GXm01bs0gbpJb&s!5kzf1<9OgrItwnIgt*69R*9u49Ii}Wd><^mX%I{xyY6k zq~vTXb!G+RBsu|h8Z0Y2AhW1F8~#m(e_)fzH907As0-g*I*;!Z%FPMNJnF@FDh=VA zPoCVMbki1mr_p763uxw)p!87R6nK>fucicK5mn^DtEuoRFCe}2CD<*ng;N8O1^G7& z^r;~yQeGIA301f+{F2q<$#nuF?Sm%)V9+r&%7cTp~$=%hn|EkT>6)PezQIFHD)Vx7rNo;b#nOa%6bFM z@E3aZT(j)v@q=d)yvxz7*j>QQ^z746U--y-(fdw8;~mjlq!;~mub$m@M=$;7*Xx%?}Zroer zS%Z;rVKPkg>p-$nX{O`zlZ-I3)=W17$zDCd{pD)U;kN6|FOAt!jupr83OC3XKAQWF zU2C8j_2Led&9{#ZWx!B3s2#j0hPKX6PSrE#yKVA~H5rw|=r$d;!8vX2WUrnQH??P$ zmB+@HjM|8C^d$Lw{k{+8{e9OJY!ABy?~jnR#_rH_-q^--oqJZ#9WY2%7oz7Ss;IHc zo77liPt`N#^v^f#c<;~7MPscp9-!?t$usm6`_+5dpPTH1RbvfCvTzYTls|jlv5tYf zF^xZ-F4ZJEv@9BH^2*trUAtENz7-ahxN(FE#PejUwabqwuhwqYbML-s4bNo1SvFIV zd=v1C^wl1bN~t9O0+rz>3(}O0v9xal&MfGr`nt)9<*V_hV(pqx+sc-;)7G`(OFea! c^s6SvvmqxPY;sb0C@XTZPK}TBhMqD03#K5G1poj5 diff --git a/dist/js/element/focusable.js b/dist/js/element/focusable.js deleted file mode 100644 index 8a34182..0000000 --- a/dist/js/element/focusable.js +++ /dev/null @@ -1,144 +0,0 @@ -// src/js/element/focusable.ts -function getFocusableElements(parent) { - return getValidElements(parent, getFocusableFilters(), false); -} -function getFocusableFilters() { - return [isDisabled, isInert, isHidden, isSummarised]; -} -function getItem(element, tabbable) { - return { - element, - tabIndex: tabbable ? getTabIndex(element) : -1 - }; -} -function getTabbableFilters() { - return [isNotTabbable, isNotTabbableRadio, ...getFocusableFilters()]; -} -function getTabbableElements(parent) { - return getValidElements(parent, getTabbableFilters(), true); -} -function getTabIndex(element) { - const tabIndex = element?.tabIndex ?? -1; - if (tabIndex < 0 && (/^(audio|details|video)$/i.test(element.tagName) || isEditable(element)) && !hasTabIndex(element)) { - return 0; - } - return tabIndex; -} -function getValidElements(parent, filters, tabbable) { - const items = Array.from(parent.querySelectorAll(selector)).map((element) => getItem(element, tabbable)).filter((item) => !filters.some((filter) => filter(item))); - if (!tabbable) { - return items.map((item) => item.element); - } - const indiced = []; - const zeroed = []; - const { length } = items; - for (let index = 0;index < length; index += 1) { - const item = items[index]; - if (item.tabIndex === 0) { - zeroed.push(item.element); - } else { - indiced[item.tabIndex] = [ - ...indiced[item.tabIndex] ?? [], - item.element - ]; - } - } - return [...indiced.flat(), ...zeroed]; -} -function hasTabIndex(element) { - return !Number.isNaN(Number.parseInt(element.getAttribute("tabindex"), 10)); -} -function isDisabled(item) { - if (/^(button|input|select|textarea)$/i.test(item.element.tagName) && isDisabledFromFieldset(item.element)) { - return true; - } - return (item.element.disabled ?? false) || item.element.getAttribute("aria-disabled") === "true"; -} -function isDisabledFromFieldset(element) { - let parent = element.parentElement; - while (parent !== null) { - if (parent instanceof HTMLFieldSetElement && parent.disabled) { - const children = Array.from(parent.children); - const { length } = children; - for (let index = 0;index < length; index += 1) { - const child = children[index]; - if (child instanceof HTMLLegendElement) { - return parent.matches("fieldset[disabled] *") ? true : !child.contains(element); - } - } - return true; - } - parent = parent.parentElement; - } - return false; -} -function isEditable(element) { - return /^(|true)$/i.test(element.getAttribute("contenteditable")); -} -function isFocusableElement(element) { - return isValidElement(element, getFocusableFilters(), false); -} -function isHidden(item) { - if ((item.element.hidden ?? false) || item.element instanceof HTMLInputElement && item.element.type === "hidden") { - return true; - } - const isDirectSummary = item.element.matches("details > summary:first-of-type"); - const nodeUnderDetails = isDirectSummary ? item.element.parentElement : item.element; - if (nodeUnderDetails?.matches("details:not([open]) *") ?? false) { - return true; - } - const style = getComputedStyle(item.element); - if (style.display === "none" || style.visibility === "hidden") { - return true; - } - const { height, width } = item.element.getBoundingClientRect(); - return height === 0 && width === 0; -} -function isInert(item) { - return (item.element.inert ?? false) || /^(|true)$/i.test(item.element.getAttribute("inert")) || item.element.parentElement !== null && isInert({ - element: item.element.parentElement, - tabIndex: -1 - }); -} -function isNotTabbable(item) { - return (item.tabIndex ?? -1) < 0; -} -function isNotTabbableRadio(item) { - if (!(item.element instanceof HTMLInputElement) || item.element.type !== "radio" || !item.element.name || item.element.checked) { - return false; - } - const parent = item.element.form ?? item.element.getRootNode?.() ?? item.element.ownerDocument; - const realName = CSS?.escape?.(item.element.name) ?? item.element.name; - const radios = Array.from(parent.querySelectorAll(`input[type="radio"][name="${realName}"]`)); - const checked = radios.find((radio) => radio.checked); - return checked !== undefined && checked !== item.element; -} -function isSummarised(item) { - return item.element instanceof HTMLDetailsElement && Array.from(item.element.children).some((child) => /^summary$/i.test(child.tagName)); -} -function isTabbableElement(element) { - return isValidElement(element, getTabbableFilters(), true); -} -function isValidElement(element, filters, tabbable) { - const item = getItem(element, tabbable); - return !filters.some((filter) => filter(item)); -} -var selector = [ - '[contenteditable]:not([contenteditable="false"])', - "[tabindex]:not(slot)", - "a[href]", - "audio[controls]", - "button", - "details", - "details > summary:first-of-type", - "input", - "select", - "textarea", - "video[controls]" -].map((selector2) => `${selector2}:not([inert])`).join(","); -export { - isTabbableElement, - isFocusableElement, - getTabbableElements, - getFocusableElements -}; diff --git a/dist/js/element/index.js b/dist/js/element/index.js deleted file mode 100644 index 263897a..0000000 --- a/dist/js/element/index.js +++ /dev/null @@ -1,307 +0,0 @@ -// src/js/element/index.ts -function getElementUnderPointer(skipIgnore) { - const elements = Array.from(document.querySelectorAll(":hover")).filter((element) => { - if (/^head$/i.test(element.tagName)) { - return false; - } - const style = getComputedStyle(element); - return skipIgnore === true || style.pointerEvents !== "none" && style.visibility !== "hidden"; - }); - return elements[elements.length - 1]; -} -function getTextDirection(element) { - const direction = element.getAttribute("dir"); - if (direction !== null && /^(ltr|rtl)$/i.test(direction)) { - return direction.toLowerCase(); - } - return getComputedStyle?.(element)?.direction === "rtl" ? "rtl" : "ltr"; -} - -// src/js/element/attribute.ts -function isBadAttribute(name, value) { - return onPrefix.test(name) || sourcePrefix.test(name) && valuePrefix.test(value); -} -function isBooleanAttribute(name) { - return booleanAttributes.includes(name.toLowerCase()); -} -function isEmptyNonBooleanAttribute(name, value) { - return !booleanAttributes.includes(name) && value.trim().length === 0; -} -function isInvalidBooleanAttribute(name, value) { - if (!booleanAttributes.includes(name)) { - return true; - } - const normalised = value.toLowerCase().trim(); - return !(normalised.length === 0 || normalised === name || name === "hidden" && normalised === "until-found"); -} -function setAttribute(element, name, value) { - if (value == null) { - element.removeAttribute(name); - } else { - element.setAttribute(name, typeof value === "string" ? value : JSON.stringify(value)); - } -} -var booleanAttributes = Object.freeze([ - "async", - "autofocus", - "autoplay", - "checked", - "controls", - "default", - "defer", - "disabled", - "formnovalidate", - "hidden", - "inert", - "ismap", - "itemscope", - "loop", - "multiple", - "muted", - "nomodule", - "novalidate", - "open", - "playsinline", - "readonly", - "required", - "reversed", - "selected" -]); -var onPrefix = /^on/i; -var sourcePrefix = /^(href|src|xlink:href)$/i; -var valuePrefix = /(data:text\/html|javascript:)/i; -// src/js/element/closest.ts -function calculateDistance(origin, target) { - if (origin === target || origin.parentElement === target) { - return 0; - } - const comparison = origin.compareDocumentPosition(target); - const children = [...origin.parentElement?.children ?? []]; - switch (true) { - case children.includes(target): - return Math.abs(children.indexOf(origin) - children.indexOf(target)); - case !!(comparison & 2 || comparison & 8): - return traverse(origin, target); - case !!(comparison & 4 || comparison & 16): - return traverse(target, origin); - default: - return -1; - } -} -function closest(origin, selector, context) { - if (origin.matches(selector)) { - return [origin]; - } - const elements = [...(context ?? document).querySelectorAll(selector)]; - const { length } = elements; - if (length === 0) { - return []; - } - const distances = []; - let minimum = null; - for (let index = 0;index < length; index += 1) { - const element = elements[index]; - const distance = calculateDistance(origin, element); - if (distance < 0) { - continue; - } - if (minimum == null || distance < minimum) { - minimum = distance; - } - distances.push({ - distance, - element - }); - } - return minimum == null ? [] : distances.filter((found) => found.distance === minimum).map((found) => found.element); -} -function traverse(from, to) { - const children = [...to.children]; - if (children.includes(from)) { - return children.indexOf(from) + 1; - } - let current = from; - let distance = 0; - let parent = from.parentElement; - while (parent != null) { - if (parent === to) { - return distance + 1; - } - const children2 = [...parent.children ?? []]; - if (children2.includes(to)) { - return distance + Math.abs(children2.indexOf(current) - children2.indexOf(to)); - } - const index = children2.findIndex((child) => child.contains(to)); - if (index > -1) { - return distance + Math.abs(index - children2.indexOf(current)) + traverse(to, children2[index]); - } - current = parent; - distance += 1; - parent = parent.parentElement; - } - return -1e6; -} -// src/js/string/index.ts -function getString(value2) { - if (typeof value2 === "string") { - return value2; - } - if (typeof value2 !== "object" || value2 == null) { - return String(value2); - } - const valueOff = value2.valueOf?.() ?? value2; - const asString = valueOff?.toString?.() ?? String(valueOff); - return asString.startsWith("[object ") ? JSON.stringify(value2) : asString; -} -function parse(value2, reviver) { - try { - return JSON.parse(value2, reviver); - } catch { - } -} -// src/js/is.ts -function isNullableOrWhitespace(value2) { - return value2 == null || /^\s*$/.test(getString(value2)); -} -function isPlainObject(value2) { - if (typeof value2 !== "object" || value2 === null) { - return false; - } - const prototype = Object.getPrototypeOf(value2); - return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value2) && !(Symbol.iterator in value2); -} - -// src/js/internal/element-value.ts -function setElementValues(element, first, second, callback) { - if (isPlainObject(first)) { - const entries = Object.entries(first); - const { length } = entries; - for (let index = 0;index < length; index += 1) { - const [key, value2] = entries[index]; - callback(element, key, value2); - } - } else if (first != null) { - callback(element, first, second); - } -} -function updateElementValue(element, key, value2, set3, remove, json) { - if (isNullableOrWhitespace(value2)) { - remove.call(element, key); - } else { - set3.call(element, key, json ? JSON.stringify(value2) : String(value2)); - } -} - -// src/js/element/data.ts -function getData(element, keys) { - if (typeof keys === "string") { - return getDataValue(element, keys); - } - const data = {}; - const { length } = keys; - for (let index = 0;index < length; index += 1) { - const key = keys[index]; - data[key] = getDataValue(element, key); - } - return data; -} -function getDataValue(element, key) { - const value2 = element.dataset[key]; - if (value2 != null) { - return parse(value2); - } -} -function setData(element, first, second) { - setElementValues(element, first, second, updateDataAttribute); -} -function updateDataAttribute(element, key, value2) { - updateElementValue(element, `data-${key}`, value2, element.setAttribute, element.removeAttribute, true); -} -// src/js/element/find.ts -function findElement(selector, context) { - return findElementOrElements(selector, context, true); -} -function findElementOrElements(selector, context, single) { - const callback = single ? document.querySelector : document.querySelectorAll; - const contexts = context == null ? [document] : findElementOrElements(context, undefined, false); - const result = []; - if (typeof selector === "string") { - const { length: length2 } = contexts; - for (let index = 0;index < length2; index += 1) { - const value2 = callback.call(contexts[index], selector); - if (single) { - if (value2 == null) { - continue; - } - return value2; - } - result.push(...Array.from(value2)); - } - return single ? undefined : result.filter((value2, index, array2) => array2.indexOf(value2) === index); - } - const nodes = Array.isArray(selector) ? selector : selector instanceof NodeList ? Array.from(selector) : [selector]; - const { length } = nodes; - for (let index = 0;index < length; index += 1) { - const node = nodes[index]; - const element = node instanceof Document ? node.body : node instanceof Element ? node : undefined; - if (element != null && (context == null || contexts.length === 0 || contexts.some((context2) => context2 === element || context2.contains(element))) && !result.includes(element)) { - result.push(element); - } - } - return result; -} -function findElements(selector, context) { - return findElementOrElements(selector, context, false); -} -function findParentElement(origin, selector) { - if (origin == null || selector == null) { - return null; - } - if (typeof selector === "string") { - if (origin.matches?.(selector)) { - return origin; - } - return origin.closest(selector); - } - if (selector(origin)) { - return origin; - } - let parent = origin.parentElement; - while (parent != null && !selector(parent)) { - if (parent === document.body) { - return null; - } - parent = parent.parentElement; - } - return parent; -} -// src/js/element/style.ts -function setStyles(element, first, second) { - setElementValues(element, first, second, updateStyleProperty); -} -function updateStyleProperty(element, key, value2) { - updateElementValue(element, key, value2, function(key2, value3) { - this.style[key2] = value3; - }, function(key2) { - this.style[key2] = ""; - }, false); -} -export { - setStyles, - setData, - setAttribute, - isInvalidBooleanAttribute, - isEmptyNonBooleanAttribute, - isBooleanAttribute, - isBadAttribute, - getTextDirection, - getElementUnderPointer, - getData, - findParentElement, - findElements, - findElement, - closest, - booleanAttributes, - findElements as $$, - findElement as $ -}; diff --git a/dist/js/html/index.js b/dist/js/html/index.js deleted file mode 100644 index 868167c..0000000 --- a/dist/js/html/index.js +++ /dev/null @@ -1,124 +0,0 @@ -// src/js/is.ts -function isPlainObject(value2) { - if (typeof value2 !== "object" || value2 === null) { - return false; - } - const prototype = Object.getPrototypeOf(value2); - return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value2) && !(Symbol.iterator in value2); -} - -// src/js/element/attribute.ts -function isBadAttribute(name, value2) { - return onPrefix.test(name) || sourcePrefix.test(name) && valuePrefix.test(value2); -} -function isEmptyNonBooleanAttribute(name, value2) { - return !booleanAttributes.includes(name) && value2.trim().length === 0; -} -function isInvalidBooleanAttribute(name, value2) { - if (!booleanAttributes.includes(name)) { - return true; - } - const normalised = value2.toLowerCase().trim(); - return !(normalised.length === 0 || normalised === name || name === "hidden" && normalised === "until-found"); -} -var booleanAttributes = Object.freeze([ - "async", - "autofocus", - "autoplay", - "checked", - "controls", - "default", - "defer", - "disabled", - "formnovalidate", - "hidden", - "inert", - "ismap", - "itemscope", - "loop", - "multiple", - "muted", - "nomodule", - "novalidate", - "open", - "playsinline", - "readonly", - "required", - "reversed", - "selected" -]); -var onPrefix = /^on/i; -var sourcePrefix = /^(href|src|xlink:href)$/i; -var valuePrefix = /(data:text\/html|javascript:)/i; -// src/js/html/sanitise.ts -function sanitise(value2, options) { - return sanitiseNodes(Array.isArray(value2) ? value2 : [value2], { - sanitiseBooleanAttributes: options?.sanitiseBooleanAttributes ?? true - }); -} -function sanitiseAttributes(element2, attributes, options) { - const { length } = attributes; - for (let index = 0;index < length; index += 1) { - const { name, value: value2 } = attributes[index]; - if (isBadAttribute(name, value2) || isEmptyNonBooleanAttribute(name, value2)) { - element2.removeAttribute(name); - } else if (options.sanitiseBooleanAttributes && isInvalidBooleanAttribute(name, value2)) { - element2.setAttribute(name, ""); - } - } -} -function sanitiseNodes(nodes, options) { - const { length } = nodes; - for (let index = 0;index < length; index += 1) { - const node = nodes[index]; - if (node instanceof Element) { - sanitiseAttributes(node, [...node.attributes], options); - } - sanitiseNodes([...node.childNodes], options); - } - return nodes; -} - -// src/js/html/index.ts -function createTemplate(html) { - const template2 = document.createElement("template"); - template2.innerHTML = html; - templates[html] = template2; - return template2; -} -function getNodes(node) { - return /^documentfragment$/i.test(node.constructor.name) ? [...node.childNodes] : [node]; -} -function getTemplate(value2) { - if (value2.trim().length === 0) { - return; - } - let template2; - if (/^[\w-]+$/.test(value2)) { - template2 = document.querySelector(`#${value2}`); - } - if (template2 instanceof HTMLTemplateElement) { - return template2; - } - return templates[value2] ?? createTemplate(value2); -} -function html(value2, sanitisation) { - const options = sanitisation == null || sanitisation === true ? {} : isPlainObject(sanitisation) ? { ...sanitisation } : null; - const template2 = value2 instanceof HTMLTemplateElement ? value2 : typeof value2 === "string" ? getTemplate(value2) : null; - if (template2 == null) { - return []; - } - const cloned = template2.content.cloneNode(true); - const scripts = cloned.querySelectorAll("script"); - const { length } = scripts; - for (let index = 0;index < length; index += 1) { - scripts[index].remove(); - } - cloned.normalize(); - return options != null ? sanitise(getNodes(cloned), options) : getNodes(cloned); -} -var templates = {}; -export { - sanitise, - html -}; diff --git a/dist/js/index.js b/dist/js/index.js index ec5fe7c..f602476 100644 --- a/dist/js/index.js +++ b/dist/js/index.js @@ -1114,402 +1114,6 @@ class HexColour { return hexToRgb(value2); } } -// src/js/element/focusable.ts -function getFocusableElements(parent) { - return getValidElements(parent, getFocusableFilters(), false); -} -function getFocusableFilters() { - return [isDisabled, isInert, isHidden, isSummarised]; -} -function getItem(element, tabbable) { - return { - element, - tabIndex: tabbable ? getTabIndex(element) : -1 - }; -} -function getTabbableFilters() { - return [isNotTabbable, isNotTabbableRadio, ...getFocusableFilters()]; -} -function getTabbableElements(parent) { - return getValidElements(parent, getTabbableFilters(), true); -} -function getTabIndex(element) { - const tabIndex = element?.tabIndex ?? -1; - if (tabIndex < 0 && (/^(audio|details|video)$/i.test(element.tagName) || isEditable(element)) && !hasTabIndex(element)) { - return 0; - } - return tabIndex; -} -function getValidElements(parent, filters, tabbable) { - const items = Array.from(parent.querySelectorAll(selector)).map((element) => getItem(element, tabbable)).filter((item) => !filters.some((filter3) => filter3(item))); - if (!tabbable) { - return items.map((item) => item.element); - } - const indiced = []; - const zeroed = []; - const { length } = items; - for (let index = 0;index < length; index += 1) { - const item = items[index]; - if (item.tabIndex === 0) { - zeroed.push(item.element); - } else { - indiced[item.tabIndex] = [ - ...indiced[item.tabIndex] ?? [], - item.element - ]; - } - } - return [...indiced.flat(), ...zeroed]; -} -function hasTabIndex(element) { - return !Number.isNaN(Number.parseInt(element.getAttribute("tabindex"), 10)); -} -function isDisabled(item) { - if (/^(button|input|select|textarea)$/i.test(item.element.tagName) && isDisabledFromFieldset(item.element)) { - return true; - } - return (item.element.disabled ?? false) || item.element.getAttribute("aria-disabled") === "true"; -} -function isDisabledFromFieldset(element) { - let parent = element.parentElement; - while (parent !== null) { - if (parent instanceof HTMLFieldSetElement && parent.disabled) { - const children = Array.from(parent.children); - const { length } = children; - for (let index = 0;index < length; index += 1) { - const child = children[index]; - if (child instanceof HTMLLegendElement) { - return parent.matches("fieldset[disabled] *") ? true : !child.contains(element); - } - } - return true; - } - parent = parent.parentElement; - } - return false; -} -function isEditable(element) { - return /^(|true)$/i.test(element.getAttribute("contenteditable")); -} -function isFocusableElement(element) { - return isValidElement(element, getFocusableFilters(), false); -} -function isHidden(item) { - if ((item.element.hidden ?? false) || item.element instanceof HTMLInputElement && item.element.type === "hidden") { - return true; - } - const isDirectSummary = item.element.matches("details > summary:first-of-type"); - const nodeUnderDetails = isDirectSummary ? item.element.parentElement : item.element; - if (nodeUnderDetails?.matches("details:not([open]) *") ?? false) { - return true; - } - const style = getComputedStyle(item.element); - if (style.display === "none" || style.visibility === "hidden") { - return true; - } - const { height, width } = item.element.getBoundingClientRect(); - return height === 0 && width === 0; -} -function isInert(item) { - return (item.element.inert ?? false) || /^(|true)$/i.test(item.element.getAttribute("inert")) || item.element.parentElement !== null && isInert({ - element: item.element.parentElement, - tabIndex: -1 - }); -} -function isNotTabbable(item) { - return (item.tabIndex ?? -1) < 0; -} -function isNotTabbableRadio(item) { - if (!(item.element instanceof HTMLInputElement) || item.element.type !== "radio" || !item.element.name || item.element.checked) { - return false; - } - const parent = item.element.form ?? item.element.getRootNode?.() ?? item.element.ownerDocument; - const realName = CSS?.escape?.(item.element.name) ?? item.element.name; - const radios = Array.from(parent.querySelectorAll(`input[type="radio"][name="${realName}"]`)); - const checked = radios.find((radio) => radio.checked); - return checked !== undefined && checked !== item.element; -} -function isSummarised(item) { - return item.element instanceof HTMLDetailsElement && Array.from(item.element.children).some((child) => /^summary$/i.test(child.tagName)); -} -function isTabbableElement(element) { - return isValidElement(element, getTabbableFilters(), true); -} -function isValidElement(element, filters, tabbable) { - const item = getItem(element, tabbable); - return !filters.some((filter3) => filter3(item)); -} -var selector = [ - '[contenteditable]:not([contenteditable="false"])', - "[tabindex]:not(slot)", - "a[href]", - "audio[controls]", - "button", - "details", - "details > summary:first-of-type", - "input", - "select", - "textarea", - "video[controls]" -].map((selector2) => `${selector2}:not([inert])`).join(","); -// src/js/element/index.ts -function getElementUnderPointer(skipIgnore) { - const elements = Array.from(document.querySelectorAll(":hover")).filter((element) => { - if (/^head$/i.test(element.tagName)) { - return false; - } - const style = getComputedStyle(element); - return skipIgnore === true || style.pointerEvents !== "none" && style.visibility !== "hidden"; - }); - return elements[elements.length - 1]; -} -function getTextDirection(element) { - const direction = element.getAttribute("dir"); - if (direction !== null && /^(ltr|rtl)$/i.test(direction)) { - return direction.toLowerCase(); - } - return getComputedStyle?.(element)?.direction === "rtl" ? "rtl" : "ltr"; -} - -// src/js/element/attribute.ts -function isBadAttribute(name, value2) { - return onPrefix.test(name) || sourcePrefix.test(name) && valuePrefix.test(value2); -} -function isBooleanAttribute(name) { - return booleanAttributes.includes(name.toLowerCase()); -} -function isEmptyNonBooleanAttribute(name, value2) { - return !booleanAttributes.includes(name) && value2.trim().length === 0; -} -function isInvalidBooleanAttribute(name, value2) { - if (!booleanAttributes.includes(name)) { - return true; - } - const normalised = value2.toLowerCase().trim(); - return !(normalised.length === 0 || normalised === name || name === "hidden" && normalised === "until-found"); -} -function setAttribute(element, name, value2) { - if (value2 == null) { - element.removeAttribute(name); - } else { - element.setAttribute(name, typeof value2 === "string" ? value2 : JSON.stringify(value2)); - } -} -var booleanAttributes = Object.freeze([ - "async", - "autofocus", - "autoplay", - "checked", - "controls", - "default", - "defer", - "disabled", - "formnovalidate", - "hidden", - "inert", - "ismap", - "itemscope", - "loop", - "multiple", - "muted", - "nomodule", - "novalidate", - "open", - "playsinline", - "readonly", - "required", - "reversed", - "selected" -]); -var onPrefix = /^on/i; -var sourcePrefix = /^(href|src|xlink:href)$/i; -var valuePrefix = /(data:text\/html|javascript:)/i; -// src/js/element/closest.ts -function calculateDistance(origin, target) { - if (origin === target || origin.parentElement === target) { - return 0; - } - const comparison2 = origin.compareDocumentPosition(target); - const children = [...origin.parentElement?.children ?? []]; - switch (true) { - case children.includes(target): - return Math.abs(children.indexOf(origin) - children.indexOf(target)); - case !!(comparison2 & 2 || comparison2 & 8): - return traverse(origin, target); - case !!(comparison2 & 4 || comparison2 & 16): - return traverse(target, origin); - default: - return -1; - } -} -function closest(origin, selector2, context) { - if (origin.matches(selector2)) { - return [origin]; - } - const elements = [...(context ?? document).querySelectorAll(selector2)]; - const { length } = elements; - if (length === 0) { - return []; - } - const distances = []; - let minimum = null; - for (let index = 0;index < length; index += 1) { - const element = elements[index]; - const distance = calculateDistance(origin, element); - if (distance < 0) { - continue; - } - if (minimum == null || distance < minimum) { - minimum = distance; - } - distances.push({ - distance, - element - }); - } - return minimum == null ? [] : distances.filter((found) => found.distance === minimum).map((found) => found.element); -} -function traverse(from, to) { - const children = [...to.children]; - if (children.includes(from)) { - return children.indexOf(from) + 1; - } - let current = from; - let distance = 0; - let parent = from.parentElement; - while (parent != null) { - if (parent === to) { - return distance + 1; - } - const children2 = [...parent.children ?? []]; - if (children2.includes(to)) { - return distance + Math.abs(children2.indexOf(current) - children2.indexOf(to)); - } - const index = children2.findIndex((child) => child.contains(to)); - if (index > -1) { - return distance + Math.abs(index - children2.indexOf(current)) + traverse(to, children2[index]); - } - current = parent; - distance += 1; - parent = parent.parentElement; - } - return -1e6; -} -// src/js/internal/element-value.ts -function setElementValues(element, first, second, callback) { - if (isPlainObject(first)) { - const entries = Object.entries(first); - const { length } = entries; - for (let index = 0;index < length; index += 1) { - const [key, value2] = entries[index]; - callback(element, key, value2); - } - } else if (first != null) { - callback(element, first, second); - } -} -function updateElementValue(element, key, value2, set3, remove, json) { - if (isNullableOrWhitespace(value2)) { - remove.call(element, key); - } else { - set3.call(element, key, json ? JSON.stringify(value2) : String(value2)); - } -} - -// src/js/element/data.ts -function getData(element, keys) { - if (typeof keys === "string") { - return getDataValue(element, keys); - } - const data = {}; - const { length } = keys; - for (let index = 0;index < length; index += 1) { - const key = keys[index]; - data[key] = getDataValue(element, key); - } - return data; -} -function getDataValue(element, key) { - const value2 = element.dataset[key]; - if (value2 != null) { - return parse(value2); - } -} -function setData(element, first, second) { - setElementValues(element, first, second, updateDataAttribute); -} -function updateDataAttribute(element, key, value2) { - updateElementValue(element, `data-${key}`, value2, element.setAttribute, element.removeAttribute, true); -} -// src/js/element/find.ts -function findElement(selector2, context) { - return findElementOrElements(selector2, context, true); -} -function findElementOrElements(selector2, context, single) { - const callback = single ? document.querySelector : document.querySelectorAll; - const contexts = context == null ? [document] : findElementOrElements(context, undefined, false); - const result = []; - if (typeof selector2 === "string") { - const { length: length2 } = contexts; - for (let index = 0;index < length2; index += 1) { - const value2 = callback.call(contexts[index], selector2); - if (single) { - if (value2 == null) { - continue; - } - return value2; - } - result.push(...Array.from(value2)); - } - return single ? undefined : result.filter((value2, index, array2) => array2.indexOf(value2) === index); - } - const nodes = Array.isArray(selector2) ? selector2 : selector2 instanceof NodeList ? Array.from(selector2) : [selector2]; - const { length } = nodes; - for (let index = 0;index < length; index += 1) { - const node = nodes[index]; - const element = node instanceof Document ? node.body : node instanceof Element ? node : undefined; - if (element != null && (context == null || contexts.length === 0 || contexts.some((context2) => context2 === element || context2.contains(element))) && !result.includes(element)) { - result.push(element); - } - } - return result; -} -function findElements(selector2, context) { - return findElementOrElements(selector2, context, false); -} -function findParentElement(origin, selector2) { - if (origin == null || selector2 == null) { - return null; - } - if (typeof selector2 === "string") { - if (origin.matches?.(selector2)) { - return origin; - } - return origin.closest(selector2); - } - if (selector2(origin)) { - return origin; - } - let parent = origin.parentElement; - while (parent != null && !selector2(parent)) { - if (parent === document.body) { - return null; - } - parent = parent.parentElement; - } - return parent; -} -// src/js/element/style.ts -function setStyles(element, first, second) { - setElementValues(element, first, second, updateStyleProperty); -} -function updateStyleProperty(element, key, value2) { - updateElementValue(element, key, value2, function(key2, value3) { - this.style[key2] = value3; - }, function(key2) { - this.style[key2] = ""; - }, false); -} // src/js/function.ts function debounce(callback, time) { const interval = clamp(time ?? 0, 0, 1000); @@ -1710,74 +1314,6 @@ function getPosition(event) { } return typeof x === "number" && typeof y === "number" ? { x, y } : undefined; } -// src/js/html/sanitise.ts -function sanitise(value2, options) { - return sanitiseNodes(Array.isArray(value2) ? value2 : [value2], { - sanitiseBooleanAttributes: options?.sanitiseBooleanAttributes ?? true - }); -} -function sanitiseAttributes(element2, attributes, options) { - const { length } = attributes; - for (let index = 0;index < length; index += 1) { - const { name, value: value2 } = attributes[index]; - if (isBadAttribute(name, value2) || isEmptyNonBooleanAttribute(name, value2)) { - element2.removeAttribute(name); - } else if (options.sanitiseBooleanAttributes && isInvalidBooleanAttribute(name, value2)) { - element2.setAttribute(name, ""); - } - } -} -function sanitiseNodes(nodes, options) { - const { length } = nodes; - for (let index = 0;index < length; index += 1) { - const node = nodes[index]; - if (node instanceof Element) { - sanitiseAttributes(node, [...node.attributes], options); - } - sanitiseNodes([...node.childNodes], options); - } - return nodes; -} - -// src/js/html/index.ts -function createTemplate(html) { - const template3 = document.createElement("template"); - template3.innerHTML = html; - templates[html] = template3; - return template3; -} -function getNodes(node) { - return /^documentfragment$/i.test(node.constructor.name) ? [...node.childNodes] : [node]; -} -function getTemplate(value2) { - if (value2.trim().length === 0) { - return; - } - let template3; - if (/^[\w-]+$/.test(value2)) { - template3 = document.querySelector(`#${value2}`); - } - if (template3 instanceof HTMLTemplateElement) { - return template3; - } - return templates[value2] ?? createTemplate(value2); -} -function html(value2, sanitisation) { - const options = sanitisation == null || sanitisation === true ? {} : isPlainObject(sanitisation) ? { ...sanitisation } : null; - const template3 = value2 instanceof HTMLTemplateElement ? value2 : typeof value2 === "string" ? getTemplate(value2) : null; - if (template3 == null) { - return []; - } - const cloned = template3.content.cloneNode(true); - const scripts = cloned.querySelectorAll("script"); - const { length } = scripts; - for (let index = 0;index < length; index += 1) { - scripts[index].remove(); - } - cloned.normalize(); - return options != null ? sanitise(getNodes(cloned), options) : getNodes(cloned); -} -var templates = {}; // src/js/logger.ts if (globalThis._atomic_logging == null) { globalThis._atomic_logging = true; @@ -1979,10 +1515,6 @@ export { smush, shuffle2 as shuffle, setValue, - setStyles, - setData, - setAttribute, - sanitise, round, queue, push, @@ -1997,7 +1529,6 @@ export { logger, kebabCase, join, - isTabbableElement, isRGBColour, isPrimitive, isPlainObject, @@ -2008,23 +1539,15 @@ export { isNullableOrEmpty, isNullable, isKey, - isInvalidBooleanAttribute, isHexColour, isHSLColour, - isFocusableElement, - isEmptyNonBooleanAttribute, isEmpty, isColour, - isBooleanAttribute, - isBadAttribute, isArrayOrPlainObject, insert, indexOf, - html, groupBy, getValue, - getTextDirection, - getTabbableElements, getString, getRandomItems, getRandomItem, @@ -2041,14 +1564,8 @@ export { getHexColour, getHSLColour, getForegroundColour, - getFocusableElements, - getElementUnderPointer, - getData, fromQuery, flatten2 as flatten, - findParentElement, - findElements, - findElement, find, filter, exists, @@ -2059,18 +1576,14 @@ export { createUuid, count, compact, - closest, clone, clamp, chunk, capitalise, camelCase, - booleanAttributes, between, average, RGBColour, HexColour, - HSLColour, - findElements as $$, - findElement as $ + HSLColour }; diff --git a/package.json b/package.json index a262d06..c903d48 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "url": "https://oscarpalmer.se" }, "dependencies": { - "@oscarpalmer/timer": "^0.21.0", "type-fest": "^4.23.0" }, "description": "Sweet little atomic goodies…", "devDependencies": { "@biomejs/biome": "^1.8.3", "@happy-dom/global-registrator": "^14.12.3", + "@oscarpalmer/timer": "^0.21.1", "@types/bun": "^1.1.6", "bun": "^1.1.21", "dts-bundle-generator": "^9.5.1", @@ -53,12 +53,6 @@ "sass": "./src/css/reset.scss", "style": "./dist/css/reset.css" }, - "./element": { - "types": "./types/element/index.d.ts", - "bun": "./src/js/element/index.ts", - "import": "./dist/js/element/index.mjs", - "require": "./dist/js/element/index.js" - }, "./emitter": { "types": "./types/emitter.d.ts", "bun": "./src/js/emitter.ts", @@ -71,24 +65,12 @@ "import": "./dist/js/event.mjs", "require": "./dist/js/event.js" }, - "./focusable": { - "types": "./types/element/focusable.d.ts", - "bun": "./src/js/element/focusable.ts", - "import": "./dist/js/element/focusable.mjs", - "require": "./dist/js/element/focusable.js" - }, "./function": { "types": "./types/function.d.ts", "bun": "./src/js/function.ts", "import": "./dist/js/function.mjs", "require": "./dist/js/function.js" }, - "./html": { - "types": "./types/html/index.d.ts", - "bun": "./src/js/html/index.ts", - "import": "./dist/js/html/index.mjs", - "require": "./dist/js/html/index.js" - }, "./is": { "types": "./types/is.d.ts", "bun": "./src/js/is.ts", @@ -187,5 +169,5 @@ }, "type": "module", "types": "./types/index.d.cts", - "version": "0.70.1" + "version": "0.71.0" } diff --git a/src/js/element/attribute.ts b/src/js/element/attribute.ts deleted file mode 100644 index 06d0724..0000000 --- a/src/js/element/attribute.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * List of boolean attributes - */ -export const booleanAttributes = Object.freeze([ - 'async', - 'autofocus', - 'autoplay', - 'checked', - 'controls', - 'default', - 'defer', - 'disabled', - 'formnovalidate', - 'hidden', - 'inert', - 'ismap', - 'itemscope', - 'loop', - 'multiple', - 'muted', - 'nomodule', - 'novalidate', - 'open', - 'playsinline', - 'readonly', - 'required', - 'reversed', - 'selected', -]); - -const onPrefix = /^on/i; -const sourcePrefix = /^(href|src|xlink:href)$/i; -const valuePrefix = /(data:text\/html|javascript:)/i; - -/** - * Is the attribute considered bad and potentially harmful? - */ -export function isBadAttribute(name: string, value: string): boolean { - return ( - onPrefix.test(name) || (sourcePrefix.test(name) && valuePrefix.test(value)) - ); -} - -/** - * Is the attribute a boolean attribute? - */ -export function isBooleanAttribute(name: string): boolean { - return booleanAttributes.includes(name.toLowerCase()); -} - -/** - * Is the attribute empty and not a boolean attribute? - */ -export function isEmptyNonBooleanAttribute( - name: string, - value: string, -): boolean { - return !booleanAttributes.includes(name) && value.trim().length === 0; -} - -/** - * - Is the attribute an invalid boolean attribute? - * - I.e., its value is not empty or the same as its name? - */ -export function isInvalidBooleanAttribute( - name: string, - value: string, -): boolean { - if (!booleanAttributes.includes(name)) { - return true; - } - - const normalised = value.toLowerCase().trim(); - - return !( - normalised.length === 0 || - normalised === name || - (name === 'hidden' && normalised === 'until-found') - ); -} - -/** - * - Sets an attribute for an element - * - If the value is nullable, the attribute is removed - */ -export function setAttribute( - element: Element, - name: string, - value: unknown, -): void { - if (value == null) { - element.removeAttribute(name); - } else { - element.setAttribute( - name, - typeof value === 'string' ? value : JSON.stringify(value), - ); - } -} diff --git a/src/js/element/closest.ts b/src/js/element/closest.ts deleted file mode 100644 index cb400ba..0000000 --- a/src/js/element/closest.ts +++ /dev/null @@ -1,115 +0,0 @@ -function calculateDistance(origin: Element, target: Element): number { - if (origin === target || origin.parentElement === target) { - return 0; - } - - const comparison = origin.compareDocumentPosition(target); - const children = [...(origin.parentElement?.children ?? [])]; - - switch (true) { - case children.includes(target): - return Math.abs(children.indexOf(origin) - children.indexOf(target)); - - case !!(comparison & 2 || comparison & 8): - // Target element is before or holds the origin element - return traverse(origin, target); - - case !!(comparison & 4 || comparison & 16): - // Origin element is before or holds the target element - return traverse(target, origin); - - default: - return -1; - } -} - -/** - * - Finds the closest elements to the origin element that matches the selector - * - Traverses up, down, and sideways in the _DOM_-tree - */ -export function closest( - origin: Element, - selector: string, - context?: Document | Element, -): Element[] { - if (origin.matches(selector)) { - return [origin]; - } - - const elements = [...(context ?? document).querySelectorAll(selector)]; - const {length} = elements; - - if (length === 0) { - return []; - } - - const distances = []; - - let minimum: number | null = null; - - for (let index = 0; index < length; index += 1) { - const element = elements[index]; - const distance = calculateDistance(origin, element); - - if (distance < 0) { - continue; - } - - if (minimum == null || distance < minimum) { - minimum = distance; - } - - distances.push({ - distance, - element, - }); - } - - return minimum == null - ? [] - : distances - .filter(found => found.distance === minimum) - .map(found => found.element); -} - -function traverse(from: Element, to: Element): number { - const children = [...to.children]; - - if (children.includes(from)) { - return children.indexOf(from) + 1; - } - - let current = from; - let distance = 0; - let parent: Element | null = from.parentElement; - - while (parent != null) { - if (parent === to) { - return distance + 1; - } - - const children = [...(parent.children ?? [])]; - - if (children.includes(to)) { - return ( - distance + Math.abs(children.indexOf(current) - children.indexOf(to)) - ); - } - - const index = children.findIndex(child => child.contains(to)); - - if (index > -1) { - return ( - distance + - Math.abs(index - children.indexOf(current)) + - traverse(to, children[index]) - ); - } - - current = parent; - distance += 1; - parent = parent.parentElement; - } - - return -1_000_000; -} diff --git a/src/js/element/data.ts b/src/js/element/data.ts deleted file mode 100644 index f883f03..0000000 --- a/src/js/element/data.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {setElementValues, updateElementValue} from '../internal/element-value'; -import type {PlainObject} from '../models'; -import {parse} from '../string'; - -/** - * Get data values from an element as an object - */ -export function getData( - element: HTMLElement, - keys: string[], -): Value; - -/** - * Get a data value from an element - */ -export function getData(element: HTMLElement, key: string): unknown; - -export function getData( - element: HTMLElement, - keys: string | string[], -): unknown { - if (typeof keys === 'string') { - return getDataValue(element, keys); - } - - const data: PlainObject = {}; - const {length} = keys; - - for (let index = 0; index < length; index += 1) { - const key = keys[index]; - - data[key] = getDataValue(element, key); - } - - return data; -} - -function getDataValue(element: HTMLElement, key: string): unknown { - const value = element.dataset[key]; - - if (value != null) { - return parse(value); - } -} - -/** - * Set data values on an element - */ -export function setData(element: HTMLElement, data: PlainObject): void; - -/** - * Set a data values on an element - */ -export function setData( - element: HTMLElement, - key: string, - value: unknown, -): void; - -export function setData( - element: HTMLElement, - first: PlainObject | string, - second?: unknown, -): void { - setElementValues(element, first, second, updateDataAttribute); -} - -function updateDataAttribute( - element: HTMLElement, - key: string, - value: unknown, -): void { - updateElementValue( - element, - `data-${key}`, - value, - element.setAttribute, - element.removeAttribute, - true, - ); -} diff --git a/src/js/element/find.ts b/src/js/element/find.ts deleted file mode 100644 index e428f2f..0000000 --- a/src/js/element/find.ts +++ /dev/null @@ -1,137 +0,0 @@ -type Selector = string | Document | Element | Element[] | NodeList; - -/** - * - Find the first element that matches the selector - * - `context` is optional and defaults to `document` - */ -export function findElement( - selector: string, - context?: Selector, -): Element | undefined { - return findElementOrElements(selector, context, true) as never; -} - -function findElementOrElements( - selector: Selector, - context: Selector | undefined, - single: boolean, -): Element | Element[] | undefined { - const callback = single ? document.querySelector : document.querySelectorAll; - - const contexts = - context == null - ? [document] - : (findElementOrElements(context, undefined, false) as Element[]); - - const result: Element[] = []; - - if (typeof selector === 'string') { - const {length} = contexts; - - for (let index = 0; index < length; index += 1) { - const value = callback.call(contexts[index], selector) as - | Element - | Element[] - | null; - - if (single) { - if (value == null) { - continue; - } - - return value; - } - - result.push(...Array.from(value as Element[])); - } - - return single - ? undefined - : result.filter((value, index, array) => array.indexOf(value) === index); - } - - const nodes = Array.isArray(selector) - ? selector - : selector instanceof NodeList - ? Array.from(selector) - : [selector]; - - const {length} = nodes; - - for (let index = 0; index < length; index += 1) { - const node = nodes[index]; - - const element = - node instanceof Document - ? node.body - : node instanceof Element - ? node - : undefined; - - if ( - element != null && - (context == null || - contexts.length === 0 || - contexts.some( - context => context === element || context.contains(element), - )) && - !result.includes(element) - ) { - result.push(element); - } - } - - return result; -} - -/** - * - Find elements that match the selector - * - If `selector` is a node or a list of nodes, they are filtered and returned - * - `context` is optional and defaults to `document` - */ -export function findElements( - selector: Selector, - context?: Selector, -): Element[] { - return findElementOrElements(selector, context, false) as never; -} - -/** - * - Find the parent element that matches the selector - * - Matches may be found by a query string or a callback - * - If no match is found, `null` is returned - */ -export function findParentElement( - origin: Element, - selector: string | ((element: Element) => boolean), -): Element | null { - if (origin == null || selector == null) { - return null; - } - - if (typeof selector === 'string') { - if (origin.matches?.(selector)) { - return origin; - } - - return origin.closest(selector); - } - - if (selector(origin)) { - return origin; - } - - let parent: Element | null = origin.parentElement; - - while (parent != null && !selector(parent)) { - if (parent === document.body) { - return null; - } - - parent = parent.parentElement; - } - - return parent; -} - -export {findElement as $, findElements as $$}; diff --git a/src/js/element/focusable.ts b/src/js/element/focusable.ts deleted file mode 100644 index dbaa7bf..0000000 --- a/src/js/element/focusable.ts +++ /dev/null @@ -1,262 +0,0 @@ -// Based on https://github.com/focus-trap/tabbable :-) - -type ElementWithTabIndex = { - element: Element; - tabIndex: number; -}; - -type Filter = (item: ElementWithTabIndex) => boolean; -type InertElement = Element & {inert: boolean}; - -const selector = [ - '[contenteditable]:not([contenteditable="false"])', - '[tabindex]:not(slot)', - 'a[href]', - 'audio[controls]', - 'button', - 'details', - 'details > summary:first-of-type', - 'input', - 'select', - 'textarea', - 'video[controls]', -] - .map(selector => `${selector}:not([inert])`) - .join(','); - -/** - * Get a list of focusable elements within a parent element - */ -export function getFocusableElements(parent: Element): Element[] { - return getValidElements(parent, getFocusableFilters(), false); -} - -function getFocusableFilters(): Filter[] { - return [isDisabled, isInert, isHidden, isSummarised]; -} - -function getItem(element: Element, tabbable: boolean): ElementWithTabIndex { - return { - element, - tabIndex: tabbable ? getTabIndex(element) : -1, - }; -} - -function getTabbableFilters(): Filter[] { - return [isNotTabbable, isNotTabbableRadio, ...getFocusableFilters()]; -} - -/** - * Get a list of tabbable elements within a parent element - */ -export function getTabbableElements(parent: Element): Element[] { - return getValidElements(parent, getTabbableFilters(), true); -} - -function getTabIndex(element: Element): number { - const tabIndex = (element as HTMLElement)?.tabIndex ?? -1; - - if ( - tabIndex < 0 && - (/^(audio|details|video)$/i.test(element.tagName) || isEditable(element)) && - !hasTabIndex(element) - ) { - return 0; - } - - return tabIndex; -} - -function getValidElements( - parent: Element, - filters: Filter[], - tabbable: boolean, -): Array { - const items: ElementWithTabIndex[] = Array.from( - parent.querySelectorAll(selector), - ) - .map(element => getItem(element, tabbable)) - .filter(item => !filters.some(filter => filter(item))); - - if (!tabbable) { - return items.map(item => item.element); - } - - const indiced: Array> = []; - const zeroed: Array = []; - const {length} = items; - - for (let index = 0; index < length; index += 1) { - const item = items[index]; - - if (item.tabIndex === 0) { - zeroed.push(item.element); - } else { - indiced[item.tabIndex] = [ - ...(indiced[item.tabIndex] ?? []), - item.element, - ]; - } - } - - return [...indiced.flat(), ...zeroed]; -} - -function hasTabIndex(element: Element): boolean { - return !Number.isNaN( - Number.parseInt(element.getAttribute('tabindex') as string, 10), - ); -} - -function isDisabled(item: ElementWithTabIndex): boolean { - if ( - /^(button|input|select|textarea)$/i.test(item.element.tagName) && - isDisabledFromFieldset(item.element) - ) { - return true; - } - - return ( - ((item.element as HTMLInputElement).disabled ?? false) || - item.element.getAttribute('aria-disabled') === 'true' - ); -} - -function isDisabledFromFieldset(element: Element): boolean { - let parent = element.parentElement; - - while (parent !== null) { - if (parent instanceof HTMLFieldSetElement && parent.disabled) { - const children = Array.from(parent.children); - const {length} = children; - - for (let index = 0; index < length; index += 1) { - const child = children[index]; - - if (child instanceof HTMLLegendElement) { - return parent.matches('fieldset[disabled] *') - ? true - : !child.contains(element); - } - } - - return true; - } - - parent = parent.parentElement; - } - - return false; -} - -function isEditable(element: Element): boolean { - return /^(|true)$/i.test(element.getAttribute('contenteditable') as string); -} - -/** - * Is the element focusable? - */ -export function isFocusableElement(element: Element): boolean { - return isValidElement(element, getFocusableFilters(), false); -} - -function isHidden(item: ElementWithTabIndex) { - if ( - ((item.element as HTMLElement).hidden ?? false) || - (item.element instanceof HTMLInputElement && item.element.type === 'hidden') - ) { - return true; - } - - const isDirectSummary = item.element.matches( - 'details > summary:first-of-type', - ); - - const nodeUnderDetails = isDirectSummary - ? item.element.parentElement - : item.element; - - if (nodeUnderDetails?.matches('details:not([open]) *') ?? false) { - return true; - } - - const style = getComputedStyle(item.element); - - if (style.display === 'none' || style.visibility === 'hidden') { - return true; - } - - const {height, width} = item.element.getBoundingClientRect(); - - return height === 0 && width === 0; -} - -function isInert(item: ElementWithTabIndex): boolean { - return ( - ((item.element as InertElement).inert ?? false) || - /^(|true)$/i.test(item.element.getAttribute('inert') as string) || - (item.element.parentElement !== null && - isInert({ - element: item.element.parentElement, - tabIndex: -1, - })) - ); -} - -function isNotTabbable(item: ElementWithTabIndex) { - return (item.tabIndex ?? -1) < 0; -} - -function isNotTabbableRadio(item: ElementWithTabIndex): boolean { - if ( - !(item.element instanceof HTMLInputElement) || - item.element.type !== 'radio' || - !item.element.name || - item.element.checked - ) { - return false; - } - - const parent = - item.element.form ?? - item.element.getRootNode?.() ?? - item.element.ownerDocument; - - const realName = CSS?.escape?.(item.element.name) ?? item.element.name; - - const radios = Array.from( - (parent as Element).querySelectorAll( - `input[type="radio"][name="${realName}"]`, - ), - ) as HTMLInputElement[]; - - const checked = radios.find(radio => radio.checked); - - return checked !== undefined && checked !== item.element; -} - -function isSummarised(item: ElementWithTabIndex) { - return ( - item.element instanceof HTMLDetailsElement && - Array.from(item.element.children).some(child => - /^summary$/i.test(child.tagName), - ) - ); -} - -/** - * Is the element tabbable? - */ -export function isTabbableElement(element: Element): boolean { - return isValidElement(element, getTabbableFilters(), true); -} - -function isValidElement( - element: Element, - filters: Filter[], - tabbable: boolean, -): boolean { - const item = getItem(element, tabbable); - - return !filters.some(filter => filter(item)); -} diff --git a/src/js/element/index.ts b/src/js/element/index.ts deleted file mode 100644 index ad302c9..0000000 --- a/src/js/element/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -type TextDirection = 'ltr' | 'rtl'; - -/** - * - Get the most specific element under the pointer - * - Ignores elements with `pointer-events: none` and `visibility: hidden` - * - If `skipIgnore` is `true`, no elements are ignored - */ -export function getElementUnderPointer( - skipIgnore?: boolean, -): Element | undefined { - const elements = Array.from(document.querySelectorAll(':hover')).filter( - element => { - if (/^head$/i.test(element.tagName)) { - return false; - } - - const style = getComputedStyle(element); - - return ( - skipIgnore === true || - (style.pointerEvents !== 'none' && style.visibility !== 'hidden') - ); - }, - ); - - return elements[elements.length - 1]; -} - -/** - * Get the text direction of an element - */ -export function getTextDirection(element: Element): TextDirection { - const direction = element.getAttribute('dir'); - - if (direction !== null && /^(ltr|rtl)$/i.test(direction)) { - return direction.toLowerCase() as TextDirection; - } - - return ( - getComputedStyle?.(element)?.direction === 'rtl' ? 'rtl' : 'ltr' - ) as TextDirection; -} - -export * from './attribute'; -export * from './closest'; -export * from './data'; -export * from './find'; -export * from './style'; diff --git a/src/js/element/style.ts b/src/js/element/style.ts deleted file mode 100644 index c101914..0000000 --- a/src/js/element/style.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {setElementValues, updateElementValue} from '../internal/element-value'; - -/** - * Set styles on an element - */ -export function setStyles( - element: HTMLElement, - styles: Partial, -): void; - -/** - * Set a style on an element - */ -export function setStyles( - element: HTMLElement, - key: keyof CSSStyleDeclaration, - value?: string, -): void; - -export function setStyles( - element: HTMLElement, - first: Partial | keyof CSSStyleDeclaration, - second?: unknown, -): void { - setElementValues(element, first as string, second, updateStyleProperty); -} - -function updateStyleProperty( - element: HTMLElement, - key: string, - value: unknown, -): void { - updateElementValue( - element, - key, - value, - function (this: HTMLElement, key: string, value: string) { - this.style[key as never] = value; - }, - function (this: HTMLElement, key: string) { - this.style[key as never] = ''; - }, - false, - ); -} diff --git a/src/js/html/index.ts b/src/js/html/index.ts deleted file mode 100644 index caf7b8c..0000000 --- a/src/js/html/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -import {isPlainObject} from '../is'; -import {sanitise, type SanitiseOptions} from './sanitise'; - -const templates: Record = {}; - -function createTemplate(html: string): HTMLTemplateElement { - const template = document.createElement('template'); - - template.innerHTML = html; - - templates[html] = template; - - return template; -} - -function getNodes(node: Node): Node[] { - return /^documentfragment$/i.test(node.constructor.name) - ? [...node.childNodes] - : [node]; -} - -function getTemplate(value: string): HTMLTemplateElement | undefined { - if (value.trim().length === 0) { - return; - } - - let template: unknown; - - if (/^[\w-]+$/.test(value)) { - template = document.querySelector(`#${value}`); - } - - if (template instanceof HTMLTemplateElement) { - return template; - } - - return templates[value] ?? createTemplate(value); -} - -/** - * - Create nodes from a string of HTML or a template element - * - If `value` doesn't contain any whitespace, it will be treated as an ID before falling back to being treated as HTML - * - If `sanitisation` is not provided, `true`, or an object, bad markup will be sanitised or removed - * - Regardless of the value of `sanitisation`, scripts will always be removed - */ -export function html( - value: string, - sanitisation?: boolean | SanitiseOptions, -): Node[]; - -/** - * - Create nodes from a template element - * - If `sanitisation` is not provided, `true`, or an object, bad markup will be sanitised or removed - * - Regardless of the value of `sanitisation`, scripts will always be removed - */ -export function html( - value: HTMLTemplateElement, - sanitisation?: boolean | SanitiseOptions, -): Node[]; - -export function html( - value: string | HTMLTemplateElement, - sanitisation?: boolean | SanitiseOptions, -): Node[] { - const options = - sanitisation == null || sanitisation === true - ? {} - : isPlainObject(sanitisation) - ? {...sanitisation} - : null; - - const template = - value instanceof HTMLTemplateElement - ? value - : typeof value === 'string' - ? getTemplate(value) - : null; - - if (template == null) { - return []; - } - - const cloned = template.content.cloneNode(true) as DocumentFragment; - - const scripts = cloned.querySelectorAll('script'); - const {length} = scripts; - - for (let index = 0; index < length; index += 1) { - scripts[index].remove(); - } - - cloned.normalize(); - - return options != null - ? sanitise(getNodes(cloned), options) - : getNodes(cloned); -} - -export * from './sanitise'; diff --git a/src/js/html/sanitise.ts b/src/js/html/sanitise.ts deleted file mode 100644 index 83954d2..0000000 --- a/src/js/html/sanitise.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - isBadAttribute, - isEmptyNonBooleanAttribute, - isInvalidBooleanAttribute, -} from '../element'; - -export type SanitiseOptions = { - /** - * - Sanitise boolean attributes? _(Defaults to `true`)_ - * - E.g. `checked="abc"` => `checked=""` - */ - sanitiseBooleanAttributes?: boolean; -}; - -/** - * - Sanitise one or more nodes _(as well as all their children)_: - * - Removes or sanitises bad attributes - */ -export function sanitise( - value: Node | Node[], - options?: Partial, -): Node[] { - return sanitiseNodes(Array.isArray(value) ? value : [value], { - sanitiseBooleanAttributes: options?.sanitiseBooleanAttributes ?? true, - }); -} - -function sanitiseAttributes( - element: Element, - attributes: Attr[], - options: SanitiseOptions, -): void { - const {length} = attributes; - - for (let index = 0; index < length; index += 1) { - const {name, value} = attributes[index]; - - if ( - isBadAttribute(name, value) || - isEmptyNonBooleanAttribute(name, value) - ) { - element.removeAttribute(name); - } else if ( - options.sanitiseBooleanAttributes && - isInvalidBooleanAttribute(name, value) - ) { - element.setAttribute(name, ''); - } - } -} - -function sanitiseNodes(nodes: Node[], options: SanitiseOptions): Node[] { - const {length} = nodes; - - for (let index = 0; index < length; index += 1) { - const node = nodes[index]; - - if (node instanceof Element) { - sanitiseAttributes(node, [...node.attributes], options); - } - - sanitiseNodes([...node.childNodes], options); - } - - return nodes; -} diff --git a/src/js/index.ts b/src/js/index.ts index 9bbbb5a..6b0ec69 100644 --- a/src/js/index.ts +++ b/src/js/index.ts @@ -1,11 +1,8 @@ export * from './array/index'; export * from './colour/index'; -export * from './element/focusable'; -export * from './element/index'; export * from './emitter'; export * from './event'; export * from './function'; -export * from './html/index'; export * from './is'; export * from './logger'; export * from './math'; diff --git a/src/js/internal/colour-property.ts b/src/js/internal/colour-property.ts deleted file mode 100644 index d5e10b3..0000000 --- a/src/js/internal/colour-property.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {GetterSetter} from '../models'; -import {clamp} from '../number'; - -export function createProperty( - store: Record, - key: string, - min: number, - max: number, -): GetterSetter { - return { - get() { - return store[key]; - }, - set(value) { - store[key] = clamp(value, min, max); - }, - }; -} diff --git a/src/js/internal/element-value.ts b/src/js/internal/element-value.ts deleted file mode 100644 index ad10dbe..0000000 --- a/src/js/internal/element-value.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {isNullableOrWhitespace, isPlainObject} from '../is'; -import type {PlainObject} from '../models'; - -export function setElementValues( - element: HTMLElement, - first: PlainObject | string, - second: unknown, - callback: (element: HTMLElement, key: string, value: unknown) => void, -): void { - if (isPlainObject(first)) { - const entries = Object.entries(first); - const {length} = entries; - - for (let index = 0; index < length; index += 1) { - const [key, value] = entries[index]; - - callback(element, key, value); - } - } else if (first != null) { - callback(element, first, second); - } -} - -export function updateElementValue( - element: HTMLElement, - key: string, - value: unknown, - set: (key: string, value: string) => void, - remove: (key: string) => void, - json: boolean, -): void { - if (isNullableOrWhitespace(value)) { - remove.call(element, key); - } else { - set.call(element, key, json ? JSON.stringify(value) : String(value)); - } -} diff --git a/test/element.test.ts b/test/element.test.ts deleted file mode 100644 index e755262..0000000 --- a/test/element.test.ts +++ /dev/null @@ -1,481 +0,0 @@ -import {expect, test} from 'bun:test'; -import {wait} from '@oscarpalmer/timer'; -import { - $, - $$, - booleanAttributes, - closest, - findElement, - findElements, - findParentElement, - getData, - getTextDirection, - isBadAttribute, - isBooleanAttribute, - isEmptyNonBooleanAttribute, - isInvalidBooleanAttribute, - setAttribute, - setData, - setStyles, -} from '../src/js/element'; - -const nonBooleanAttributes = [ - 'abbr', - 'accept', - 'accept-charset', - 'accesskey', - 'action', - 'allow', - 'alt', - 'as', - 'autocapitalize', - 'autocomplete', - 'blocking', - 'charset', - 'cite', - 'class', - 'color', - 'cols', - 'colspan', - 'content', - 'contenteditable', - 'coords', - 'crossorigin', - 'data', - 'datetime', - 'decoding', - 'dir', - 'dirname', - 'download', - 'draggable', - 'enctype', - 'enterkeyhint', - 'fetchpriority', - 'for', - 'form', - 'formaction', - 'formenctype', - 'formmethod', - 'formtarget', - 'headers', - 'height', - 'high', - 'href', - 'hreflang', - 'http-equiv', - 'id', - 'imagesizes', - 'imagesrcset', - 'inputmode', - 'integrity', - 'is', - 'itemid', - 'itemprop', - 'itemref', - 'itemtype', - 'kind', - 'label', - 'lang', - 'list', - 'loading', - 'low', - 'max', - 'maxlength', - 'media', - 'method', - 'min', - 'minlength', - 'name', - 'nonce', - 'optimum', - 'pattern', - 'ping', - 'placeholder', - 'popover', - 'popovertarget', - 'popovertargetaction', - 'poster', - 'preload', - 'referrerpolicy', - 'rel', - 'rows', - 'rowspan', - 'sandbox', - 'scope', - 'shape', - 'size', - 'sizes', - 'slot', - 'span', - 'spellcheck', - 'src', - 'srcdoc', - 'srclang', - 'srcset', - 'start', - 'step', - 'style', - 'tabindex', - 'target', - 'title', - 'translate', - 'type', - 'usemap', - 'value', - 'width', - 'wrap', -]; - -document.body.innerHTML = `
-
- -
-
`; - -test('closest', () => { - const template = ` -
-
-
-
bad
-
-
-
-
good
-
-
-
-

origin

-
-
-
-

good

-
-
-
-
-
-
- -
    -
  • 1
  • -
  • 2
  • -
  • 3
  • -
  • 4
  • -
  • 5
  • -
  • 6
  • -
  • 7
  • -
- -
    -
  • -
  • -
  • -
  • -
  • -
-`; - - const element = document.createElement('div'); - - element.innerHTML = template; - - const divOrigin = $('div.origin', element); - const liOrigin = $('li.origin', element); - const buttonOrigin = $('button.origin', element); - - if (divOrigin == null || liOrigin == null || buttonOrigin == null) { - return; - } - - const divTargets = closest(divOrigin, 'div.target', element); - const liTargets = closest(liOrigin, 'li.target', element); - const buttonTargets = closest(buttonOrigin, 'li.target', element); - - expect(divTargets.length).toBe(2); - expect(divTargets.map(target => target.textContent?.trim())).toEqual([ - 'good', - 'good', - ]); - - expect(liTargets.length).toBe(2); - expect(liTargets.map(target => target.textContent?.trim())).toEqual([ - '3', - '5', - ]); - - expect(buttonTargets.length).toBe(1); - expect(buttonTargets[0].textContent?.trim()).toBe('good'); - - expect(closest(buttonOrigin, 'button.origin', element)[0]).toBe(buttonOrigin); - - expect(closest(liOrigin, '.not-found').length).toBe(0); -}); - -test('findElement', () => { - const target = findElement('.target'); - - expect(target).toBeInstanceOf(HTMLDivElement); - expect(target?.classList.contains('target') ?? false).toBe(true); - - const origin = findElement('#origin', '.target'); - - expect(origin).toBeInstanceOf(HTMLDivElement); - - const child = $('*', origin); - - expect(child).toBeInstanceOf(HTMLSpanElement); -}); - -test('findElements', () => { - const elements = findElements('.target'); - - expect(elements.length).toBe(1); - expect(elements[0].classList.contains('target')).toBe(true); - - const origin = findElements('#origin', '.target'); - - expect(origin.length).toBe(1); - - const children = $$('*', origin); - - expect(children.length).toBe(1); - - expect( - // @ts-expect-error Testing invalid input - findElements([123, 'a', document.body, ...origin, ...children], document) - .length, - ).toBe(3); -}); - -test('findParentElement', () => { - const hidden = document.querySelector('[hidden]'); - const origin = document.getElementById('origin'); - const target = document.querySelector('.target'); - - if (origin === null) { - return; - } - - expect(findParentElement(origin, '#origin')).toBe(origin); - expect(findParentElement(origin, '.target')).toBe(target); - - expect(findParentElement(origin, element => element.id === 'origin')).toBe( - origin, - ); - - expect( - findParentElement(origin, element => (element as HTMLElement).hidden), - ).toBe(hidden); - - expect(findParentElement(origin, 'noop')).toBe(null); - - expect(findParentElement(origin, element => element.tagName === 'noop')).toBe( - null, - ); - - // @ts-expect-error Testing invalid input - expect(findParentElement(null, 'span')).toBe(null); -}); - -test('getData & setData', done => { - const div = document.createElement('div'); - - setData(div, 'test', 'value'); - - setData(div, { - foo: ['bar', 1, true], - bar: {baz: true}, - }); - - div.dataset.badJson = '""?""'; - - wait(() => { - expect(getData(div, 'test')).toBe('value'); - expect(getData(div, 'noop')).toBe(undefined); - expect(getData(div, 'badJson')).toBe(undefined); - - const data = getData(div, ['foo', 'bar']); - - expect(data.foo).toEqual(['bar', 1, true]); - expect(data.bar).toEqual({baz: true}); - - done(); - }, 125); -}); - -test('getElementUnderPointer', () => { - const origin = document.getElementById('origin'); - const hover = document.getElementById('hover'); - - if (origin == null || hover == null) { - return; - } - - // TODO: investigate proper way to test this - // expect(getElementUnderPointer()).toBe(origin); - // expect(getElementUnderPointer(true)).toBe(hover); -}); - -test('getTextDirection', () => { - const fragment = document.createDocumentFragment(); - const parent = document.createElement('div'); - - fragment.appendChild(parent); - - parent.id = 'parent'; - - parent.innerHTML = `
-
- -
-
`; - - const parentElement = fragment.getElementById('parent'); - const outerElement = fragment.getElementById('outer'); - const innerElement = fragment.getElementById('inner'); - const textElement = fragment.getElementById('text'); - - if ( - parentElement === null || - outerElement === null || - innerElement === null || - textElement === null - ) { - return; - } - - expect(getTextDirection(parentElement)).toBe('ltr'); - expect(getTextDirection(outerElement)).toBe('rtl'); - expect(getTextDirection(textElement)).toBe('ltr'); - - // Should be inherited from parent and be 'rtl', but does not seem to be; Happy DOM? - expect(getTextDirection(innerElement)).toBe('ltr'); -}); - -test('isBadAttribute', () => { - const attributes = [ - ['onclick', 'alert()'], - ['href', 'data:text/html,'], - ['src', 'javascript:'], - ['xlink:href', 'javascript:'], - ['href', 'https://example.com'], - ['src', 'https://example.com'], - ['xlink:href', 'https://example.com'], - ]; - - const {length} = attributes; - - for (let index = 0; index < length; index += 1) { - const [name, value] = attributes[index]; - - expect(isBadAttribute(name, value)).toBe(index < 4); - } -}); - -test('isBooleanAttribute', () => { - const attributes = [...booleanAttributes, ...nonBooleanAttributes]; - - const {length} = attributes; - - for (let index = 0; index < length; index += 1) { - const attribute = attributes[index]; - - expect(isBooleanAttribute(attribute)).toBe( - index < booleanAttributes.length, - ); - } -}); - -test('isEmptyNonBooleanAttribute', () => { - let {length} = booleanAttributes; - - for (let index = 0; index < length; index += 1) { - const attribute = booleanAttributes[index]; - - expect(isEmptyNonBooleanAttribute(attribute, '')).toBe(false); - } - - length = nonBooleanAttributes.length; - - for (let index = 0; index < length; index += 1) { - const attribute = nonBooleanAttributes[index]; - - expect(isEmptyNonBooleanAttribute(attribute, '')).toBe(true); - expect(isEmptyNonBooleanAttribute(attribute, ' ')).toBe(true); - expect(isEmptyNonBooleanAttribute(attribute, attribute)).toBe(false); - } -}); - -test('isInvalidBooleanAttribute', () => { - let {length} = booleanAttributes; - - for (let index = 0; index < length; index += 1) { - const attribute = booleanAttributes[index]; - - expect(isInvalidBooleanAttribute(attribute, '')).toBe(false); - expect(isInvalidBooleanAttribute(attribute, attribute)).toBe(false); - expect(isInvalidBooleanAttribute(attribute, '!')).toBe(true); - } - - length = nonBooleanAttributes.length; - - for (let index = 0; index < length; index += 1) { - const attribute = nonBooleanAttributes[index]; - - expect(isInvalidBooleanAttribute(attribute, '')).toBe(true); - expect(isInvalidBooleanAttribute(attribute, attribute)).toBe(true); - expect(isInvalidBooleanAttribute(attribute, '!')).toBe(true); - } -}); - -test('setAttribute', done => { - const element = document.createElement('div'); - - setAttribute(element, 'hidden', ''); - - wait(() => { - expect(element.getAttribute('hidden')).toBe(''); - - setAttribute(element, 'hidden', null); - - wait(() => { - expect(element.getAttribute('hidden')).toBe(null); - - done(); - }); - }); -}); - -test('setStyles', done => { - const div = document.createElement('div'); - - div.style.display = 'none'; - - setStyles(div, 'color', 'red'); - - setStyles(div, { - backgroundColor: 'green', - position: 'absolute', - }); - - wait(() => { - expect(div.style.color).toBe('red'); - expect(div.style.display).toBe('none'); - expect(div.style.backgroundColor).toBe('green'); - expect(div.style.position).toBe('absolute'); - - setStyles(div, 'display'); - - wait(() => { - expect(div.style.display).toBe(''); - - done(); - }, 125); - }, 125); -}); diff --git a/test/html.test.ts b/test/html.test.ts deleted file mode 100644 index 8d2e037..0000000 --- a/test/html.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {expect, test} from 'bun:test'; -import {html} from '../src/js/html'; - -test('html + sanitise', () => { - const original = ``; - - const expectedNodes = html(original, false); - - expect(expectedNodes.length).toBe(1); - expect(expectedNodes.join()).toBe(original); - - // - - const sanitised = ``; - - const sanitisedNodes = html(original); - - expect(sanitisedNodes.length).toBe(1); - expect(sanitisedNodes.join()).toBe(sanitised); - - // - - const template = document.createElement('template'); - - template.id = 'tpl'; - template.innerHTML = '

Hello

'; - - document.append(template); - - const external = html('tpl'); - - template.remove(); - - expect(external.length).toBe(1); - expect(external.join()).toBe('

Hello

'); - - // - - expect(html('')).toEqual([]); -}); - -test('sanitise', () => {}); diff --git a/test/string.test.ts b/test/string.test.ts index 4a4df96..b78ccae 100644 --- a/test/string.test.ts +++ b/test/string.test.ts @@ -6,6 +6,7 @@ import { getString, join, kebabCase, + parse, pascalCase, snakeCase, template, @@ -113,6 +114,21 @@ test('kebabCase', () => { expect(kebabCase('Product XMLs')).toBe('product-xmls'); }); +test('parse', () => { + expect(parse('')).toBe(undefined); + expect(parse('null')).toBe(null); + expect(parse('undefined')).toBe(undefined); + expect(parse('true')).toBe(true); + expect(parse('false')).toBe(false); + expect(parse('0')).toBe(0); + expect(parse('1')).toBe(1); + expect(parse('1.5')).toBe(1.5); + expect(parse('[]')).toEqual([]); + expect(parse('[1,2,3]')).toEqual([1, 2, 3]); + expect(parse('{}')).toEqual({}); + expect(parse('{"a":1}')).toEqual({a: 1}); +}); + test('pascalCase', () => { expect(pascalCase('12 feet')).toBe('12Feet'); expect(pascalCase('enable 6h format')).toBe('Enable6hFormat'); diff --git a/types/element/attribute.d.ts b/types/element/attribute.d.ts deleted file mode 100644 index f3f1f42..0000000 --- a/types/element/attribute.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * List of boolean attributes - */ -export declare const booleanAttributes: readonly string[]; -/** - * Is the attribute considered bad and potentially harmful? - */ -export declare function isBadAttribute(name: string, value: string): boolean; -/** - * Is the attribute a boolean attribute? - */ -export declare function isBooleanAttribute(name: string): boolean; -/** - * Is the attribute empty and not a boolean attribute? - */ -export declare function isEmptyNonBooleanAttribute(name: string, value: string): boolean; -/** - * - Is the attribute an invalid boolean attribute? - * - I.e., its value is not empty or the same as its name? - */ -export declare function isInvalidBooleanAttribute(name: string, value: string): boolean; -/** - * - Sets an attribute for an element - * - If the value is nullable, the attribute is removed - */ -export declare function setAttribute(element: Element, name: string, value: unknown): void; diff --git a/types/element/closest.d.ts b/types/element/closest.d.ts deleted file mode 100644 index 253ba76..0000000 --- a/types/element/closest.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * - Finds the closest elements to the origin element that matches the selector - * - Traverses up, down, and sideways in the _DOM_-tree - */ -export declare function closest(origin: Element, selector: string, context?: Document | Element): Element[]; diff --git a/types/element/data.d.ts b/types/element/data.d.ts deleted file mode 100644 index 2e0a438..0000000 --- a/types/element/data.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { PlainObject } from '../models'; -/** - * Get data values from an element as an object - */ -export declare function getData(element: HTMLElement, keys: string[]): Value; -/** - * Get a data value from an element - */ -export declare function getData(element: HTMLElement, key: string): unknown; -/** - * Set data values on an element - */ -export declare function setData(element: HTMLElement, data: PlainObject): void; -/** - * Set a data values on an element - */ -export declare function setData(element: HTMLElement, key: string, value: unknown): void; diff --git a/types/element/find.d.ts b/types/element/find.d.ts deleted file mode 100644 index d9f58e6..0000000 --- a/types/element/find.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -type Selector = string | Document | Element | Element[] | NodeList; -/** - * - Find the first element that matches the selector - * - `context` is optional and defaults to `document` - */ -export declare function findElement(selector: string, context?: Selector): Element | undefined; -/** - * - Find elements that match the selector - * - If `selector` is a node or a list of nodes, they are filtered and returned - * - `context` is optional and defaults to `document` - */ -export declare function findElements(selector: Selector, context?: Selector): Element[]; -/** - * - Find the parent element that matches the selector - * - Matches may be found by a query string or a callback - * - If no match is found, `null` is returned - */ -export declare function findParentElement(origin: Element, selector: string | ((element: Element) => boolean)): Element | null; -export { findElement as $, findElements as $$ }; diff --git a/types/element/focusable.d.ts b/types/element/focusable.d.ts deleted file mode 100644 index b4ff30e..0000000 --- a/types/element/focusable.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Get a list of focusable elements within a parent element - */ -export declare function getFocusableElements(parent: Element): Element[]; -/** - * Get a list of tabbable elements within a parent element - */ -export declare function getTabbableElements(parent: Element): Element[]; -/** - * Is the element focusable? - */ -export declare function isFocusableElement(element: Element): boolean; -/** - * Is the element tabbable? - */ -export declare function isTabbableElement(element: Element): boolean; diff --git a/types/element/index.d.ts b/types/element/index.d.ts deleted file mode 100644 index a65fa6c..0000000 --- a/types/element/index.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -type TextDirection = 'ltr' | 'rtl'; -/** - * - Get the most specific element under the pointer - * - Ignores elements with `pointer-events: none` and `visibility: hidden` - * - If `skipIgnore` is `true`, no elements are ignored - */ -export declare function getElementUnderPointer(skipIgnore?: boolean): Element | undefined; -/** - * Get the text direction of an element - */ -export declare function getTextDirection(element: Element): TextDirection; -export * from './attribute'; -export * from './closest'; -export * from './data'; -export * from './find'; -export * from './style'; diff --git a/types/element/style.d.ts b/types/element/style.d.ts deleted file mode 100644 index 2c38ac2..0000000 --- a/types/element/style.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Set styles on an element - */ -export declare function setStyles(element: HTMLElement, styles: Partial): void; -/** - * Set a style on an element - */ -export declare function setStyles(element: HTMLElement, key: keyof CSSStyleDeclaration, value?: string): void; diff --git a/types/html/index.d.ts b/types/html/index.d.ts deleted file mode 100644 index 1040ea5..0000000 --- a/types/html/index.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type SanitiseOptions } from './sanitise'; -/** - * - Create nodes from a string of HTML or a template element - * - If `value` doesn't contain any whitespace, it will be treated as an ID before falling back to being treated as HTML - * - If `sanitisation` is not provided, `true`, or an object, bad markup will be sanitised or removed - * - Regardless of the value of `sanitisation`, scripts will always be removed - */ -export declare function html(value: string, sanitisation?: boolean | SanitiseOptions): Node[]; -/** - * - Create nodes from a template element - * - If `sanitisation` is not provided, `true`, or an object, bad markup will be sanitised or removed - * - Regardless of the value of `sanitisation`, scripts will always be removed - */ -export declare function html(value: HTMLTemplateElement, sanitisation?: boolean | SanitiseOptions): Node[]; -export * from './sanitise'; diff --git a/types/html/sanitise.d.ts b/types/html/sanitise.d.ts deleted file mode 100644 index 8fa9a60..0000000 --- a/types/html/sanitise.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type SanitiseOptions = { - /** - * - Sanitise boolean attributes? _(Defaults to `true`)_ - * - E.g. `checked="abc"` => `checked=""` - */ - sanitiseBooleanAttributes?: boolean; -}; -/** - * - Sanitise one or more nodes _(as well as all their children)_: - * - Removes or sanitises bad attributes - */ -export declare function sanitise(value: Node | Node[], options?: Partial): Node[]; diff --git a/types/index.d.ts b/types/index.d.ts index 9bbbb5a..6b0ec69 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,11 +1,8 @@ export * from './array/index'; export * from './colour/index'; -export * from './element/focusable'; -export * from './element/index'; export * from './emitter'; export * from './event'; export * from './function'; -export * from './html/index'; export * from './is'; export * from './logger'; export * from './math'; diff --git a/types/internal/colour-property.d.ts b/types/internal/colour-property.d.ts deleted file mode 100644 index 60252f8..0000000 --- a/types/internal/colour-property.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import type { GetterSetter } from '../models'; -export declare function createProperty(store: Record, key: string, min: number, max: number): GetterSetter; diff --git a/types/internal/element-value.d.ts b/types/internal/element-value.d.ts deleted file mode 100644 index 51b0cb5..0000000 --- a/types/internal/element-value.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { PlainObject } from '../models'; -export declare function setElementValues(element: HTMLElement, first: PlainObject | string, second: unknown, callback: (element: HTMLElement, key: string, value: unknown) => void): void; -export declare function updateElementValue(element: HTMLElement, key: string, value: unknown, set: (key: string, value: string) => void, remove: (key: string) => void, json: boolean): void;