jessquery
Version:
Modern JavaScript is pretty good, but typing document.querySelector() is a pain. This is a tiny library that makes DOM manipulation easy. jQuery is around 80kb (30kb gzipped), while this is only around 8kb (3.5kb gzipped). Lots of JSDoc comments so it's s
265 lines (210 loc) • 8.13 kB
JavaScript
import { createQueue, handlerMaker, queueAndReturn } from "./core.js"
import { errorHandler, giveContext } from "./errors.js"
import { fetchElements, send, wrappedFetch } from "./ajax.js"
import {
addStyleSheet,
attach,
become,
Class,
moveOrClone,
parseArgument,
setFormElementValue,
} from "./DOM.js"
export function addMethods(selector, target, fixed = false) {
const originalTarget = [...target]
let proxy = null
const { addToQueue, defer } = createQueue()
const queueFunction = queueAndReturn(addToQueue, () => proxy)
const makeMethod = (action, context) => {
return queueFunction(async (...args) => {
await Promise.all(target.map((el) => action(el, ...args)))
}, giveContext(context, selector))
}
const customMethods = {
on: makeMethod((el, ev, fn) => {
el.addEventListener(ev, fn)
}, "on"),
once: makeMethod((el, ev, fn) => {
el.addEventListener(ev, fn, { once: true })
}, "once"),
delegate: makeMethod((el, ev, selector, fn) => {
el.addEventListener(ev, (event) => {
if (event.target.matches(selector)) {
fn(event)
}
})
}, "delegate"),
off: makeMethod((el, ev, fn) => {
el.removeEventListener(ev, fn)
}, "off"),
html: makeMethod((el, newHtml, outer) => {
if (outer) {
const nextSibling = el.nextSibling // We need to get the nextSibling before removing the element
el.outerHTML = newHtml // Otherwise, we lose the reference, and the proxy is empty
const newElement = nextSibling // If nextSibling is null, then we're at the end of the list
? nextSibling.previousSibling // So, we get the previousSibling from where we were
: el.parentElement.lastElementChild // Otherwise, we get the lastElementChild from the parent
const index = target.indexOf(el)
target[index] = newElement
} else {
el.innerHTML = newHtml
}
}, "html"),
text: makeMethod((el, newText) => (el.textContent = newText), "text"),
sanitize: makeMethod(
(el, newHtml, sanitizer) => el.setHTML(newHtml, sanitizer),
"sanitize"
),
val: makeMethod((el, newValue) => setFormElementValue(el, newValue), "val"),
css: makeMethod(
(el, prop, value) => parseArgument(el.style, prop, value),
"css"
),
addStyleSheet: makeMethod(
(_, rules) => addStyleSheet(rules),
"addStyleSheet"
),
addClass: makeMethod(Class("add"), "addClass"),
removeClass: makeMethod(Class("remove"), "removeClass"),
toggleClass: makeMethod(Class("toggle"), "toggleClass"),
set: makeMethod(
(el, attr, value = "") => parseArgument(el, attr, value, true),
"set"
),
unset: makeMethod((el, attr) => el.removeAttribute(attr), "unset"),
toggle: makeMethod((el, attr) => el.toggleAttribute(attr), "toggle"),
data: makeMethod(
(el, key, value) => parseArgument(el.dataset, key, value),
"data"
),
attach: makeMethod((el, ...children) => attach(el, ...children), "attach"),
cloneTo: makeMethod((el, parentSelector, options) => {
moveOrClone(el, parentSelector, { mode: "clone", ...options })
}, "cloneTo"),
moveTo: makeMethod((el, parentSelector, options) => {
moveOrClone(el, parentSelector, options)
}, "moveTo"),
become: makeMethod((el, replacements, options) => {
become(el, replacements, options)
}, "become"),
purge: makeMethod((el) => el.remove(), "purge"),
send: makeMethod((el, options) => send(el, options, target), "send"),
do: makeMethod(async (el, fn) => {
const wrappedElement = addMethods(selector, [el])
return await fn(wrappedElement)
}, "do"),
defer: makeMethod((el, fn) => {
const wrappedElement = addMethods(selector, [el])
return defer(fn, [wrappedElement])
}, "defer"),
fromJSON: makeMethod((el, url, fn, options = {}) => {
return wrappedFetch(url, options, "json", target).then((json) => {
const wrappedElement = addMethods(selector, [el])
fn(wrappedElement, json)
})
}, "fromJSON"),
fromHTML: makeMethod((el, url, options = {}) => {
return fetchElements("text", url, options, [el])
}, "fromHTML"),
fromStream: makeMethod((el, url, options = {}) => {
const type = options.sse ? "sse" : "stream"
return fetchElements(type, url, options, [el])
}, "fromStream"),
transition: makeMethod((el, keyframes, options) => {
return el.animate(keyframes, options).finished
}, "transition"),
wait: makeMethod((el, duration) => {
return new Promise((resolve) => setTimeout(resolve, duration))
}, "wait"),
next: contextSwitch("nextElementSibling"),
prev: contextSwitch("previousElementSibling"),
first: contextSwitch("firstElementChild"),
last: contextSwitch("lastElementChild"),
parent: contextSwitch("parentElement"),
ancestor: queueFunction((selector) => {
const ancestor = filterTarget((el) => el.closest(selector))
return switchTarget(ancestor)
}, giveContext("ancestor", selector)),
kids: queueFunction(() => {
const kidsArray = filterTarget((el) => Array.from(el.children))
return switchTarget(kidsArray.flat())
}, giveContext("kids", selector)),
siblings: queueFunction(() => {
const siblings = filterTarget((el) =>
Array.from(el.parentElement.children).filter((child) => child !== el)
)
return switchTarget(siblings)
}, giveContext("siblings", selector)),
pick: queueFunction((selector) => {
const pickedElements = filterTarget((el) => el.querySelector(selector))
return switchTarget(pickedElements)
}, giveContext("pick", selector)),
pickAll: queueFunction((selector) => {
const pickedElements = filterTarget((el) =>
Array.from(el.querySelectorAll(selector))
)
return switchTarget(pickedElements.flat())
}, giveContext("pickAll", selector)),
if: queueFunction(
({ is, then, or }) => {
for (const el of target) {
const wrappedElement = addMethods(selector, [el])
try {
if (is(wrappedElement)) {
then && then(wrappedElement)
} else {
or && or(wrappedElement)
}
} catch (error) {
errorHandler(error, giveContext("if", selector))
}
}
},
giveContext("if", selector),
false
),
takeWhile: queueFunction((predicate, reverse) => {
const result = []
if (reverse) target.reverse()
for (const el of target) {
try {
if (predicate(el.raw || el)) {
result.push(el)
} else {
break
}
} catch (error) {
errorHandler(error, giveContext("takeWhile", selector))
}
}
return switchTarget(result)
}, giveContext("takeWhile", selector)),
refresh: queueFunction(() => {
target = [...originalTarget]
}, giveContext("refresh", selector)),
}
function filterTarget(action) {
return target.map(action).filter((el) => el)
}
function switchTarget(newTarget) {
if (fixed)
throw new Error(`Proxy is fixed. Create new proxy to switch targets.`)
target = newTarget.length > 0 ? newTarget : []
proxy = updateProxy(target)
return proxy
}
function contextSwitch(prop) {
return queueFunction(() => {
const resultElements = filterTarget((el) => el[prop])
return switchTarget(resultElements)
}, giveContext(prop, selector))
}
function updateProxy(newTarget) {
const handler = handlerMaker(newTarget, customMethods)
const proxy = new Proxy(customMethods, handler)
proxy.raw = newTarget
return proxy
}
proxy = updateProxy(target)
return proxy
}