├── DOM.js ├── README.md ├── ajax.js ├── core.js ├── errors.js ├── examples ├── arrayMethods.html ├── attach.html ├── attach2.html ├── basicUsage.html ├── become.html ├── become2.html ├── become3.html ├── benchmark.html ├── cloneTo.html ├── contextSwitching.html ├── createElement.html ├── defer.html ├── defer2.html ├── do.html ├── doublecash.html ├── errors.html ├── fromHTML.html ├── fromJSON.html ├── fromStream.html ├── html.html ├── if.html ├── if2.html ├── moveOrClone.html ├── moveTo2.html ├── moveTo3.html ├── oldBadTests.html ├── parentKids.html ├── perilsFixedOrNot.html ├── perilsOfWait.html ├── playground.html ├── promisify - audio API.html ├── promisify - canvas.html ├── promisify - fileReader.html ├── promisify - geolocation.html ├── promisify - indexedDB.html ├── promisify - rAF.html ├── promisify - setInterval.html ├── promisify - xhr elaborate.html ├── promisify - xhr.html ├── runScripts.html ├── sanitize.html ├── send.html ├── send2.html ├── send3.html ├── server.js ├── takeWhile.html └── val.html ├── index.d.ts ├── index.js ├── methods.js ├── package-lock.json ├── package.json ├── promisify.js └── tsconfig.json /DOM.js: -------------------------------------------------------------------------------- 1 | export function setFormElementValue(element, value) { 2 | if (element instanceof HTMLInputElement) { 3 | const inputTypes = { 4 | checkbox: () => (element.checked = !!value), 5 | radio: () => (element.checked = element.value === value), 6 | default: () => (element.value = value), 7 | } 8 | const handler = inputTypes[element.type] || inputTypes.default 9 | return handler() 10 | } else if (element instanceof HTMLSelectElement) { 11 | if (element.multiple && Array.isArray(value)) { 12 | for (let option of element.options) { 13 | option.selected = value.includes(option.value) 14 | } 15 | } else if (typeof value === "string" || typeof value === "number") { 16 | element.value = value.toString() 17 | } 18 | } else if ( 19 | element instanceof HTMLTextAreaElement || 20 | element instanceof HTMLButtonElement 21 | ) { 22 | element.value = value 23 | } else { 24 | element.textContent = value 25 | } 26 | } 27 | 28 | export function parseArgument(prop, stringOrObj, value, attr) { 29 | if (typeof stringOrObj === "string") { 30 | attr ? prop.setAttribute(stringOrObj, value) : (prop[stringOrObj] = value) 31 | } else if (typeof stringOrObj === "object") { 32 | Object.assign(prop, stringOrObj) 33 | } 34 | } 35 | 36 | export function addStyleSheet(rules) { 37 | const importantRules = rules.trim().split(";").join(" !important;") 38 | const style = document.createElement("style") 39 | style.textContent = importantRules 40 | document.head.appendChild(style) 41 | } 42 | 43 | export function Class(type) { 44 | return (el, ...classes) => el.classList[type](...classes) 45 | } 46 | 47 | export function attach(element, ...args) { 48 | const options = 49 | args[args.length - 1] instanceof Object && 50 | ("sanitize" in args[args.length - 1] || "position" in args[args.length - 1]) 51 | ? args.pop() 52 | : {} 53 | 54 | const children = args.flat() 55 | 56 | if ((children instanceof NodeList || Array.isArray(children)) && !options) { 57 | options.all = true 58 | } 59 | 60 | modifyDOM(element, children, options) 61 | } 62 | 63 | export function moveOrClone(elements, parentSelector, options = {}) { 64 | let parents = getDOMElement(parentSelector, options) 65 | 66 | const children = Array.isArray(elements) ? elements : [elements].flat() 67 | 68 | parents.forEach((parent) => { 69 | modifyDOM(parent, children, options) 70 | }) 71 | } 72 | 73 | export function become(elements, replacements, options = { mode: "clone" }) { 74 | const handleReplacement = (element, replacement) => { 75 | if (!replacement) return 76 | const newElement = 77 | options.mode === "clone" ? replacement.cloneNode(true) : replacement 78 | element.replaceWith(newElement) 79 | } 80 | const proxyOrDOM = (candidate) => candidate.raw || candidate 81 | const makeArray = (candidate) => { 82 | if (Array.isArray(candidate)) return candidate 83 | if (candidate instanceof HTMLElement) return [candidate] 84 | if (candidate instanceof NodeList) return Array.from(candidate) 85 | return [] 86 | } 87 | 88 | const elementsArray = makeArray(proxyOrDOM(elements)) 89 | const replacementsArray = makeArray(proxyOrDOM(replacements)) 90 | 91 | elementsArray.forEach((element, index) => 92 | handleReplacement(element, replacementsArray[index]) 93 | ) 94 | } 95 | 96 | export function modifyDOM(parent, children, options) { 97 | const { 98 | position = "append", 99 | sanitize = true, 100 | mode = "move", 101 | sanitizer, 102 | all, 103 | } = options 104 | 105 | const DOMActions = { 106 | append: (parent, child) => parent.append(child), 107 | prepend: (parent, child) => parent.prepend(child), 108 | before: (parent, child) => parent.before(child), 109 | after: (parent, child) => parent.after(child), 110 | } 111 | 112 | const getCloneOrNode = 113 | mode === "clone" ? (el) => el.cloneNode(true) : (el) => el 114 | 115 | children.forEach((child) => { 116 | const domElements = getDOMElement(child, { sanitize, sanitizer, all }) 117 | domElements.forEach((domElement) => { 118 | DOMActions[position](parent, getCloneOrNode(domElement)) 119 | }) 120 | }) 121 | } 122 | 123 | export function getDOMElement(item, options) { 124 | return typeof item === "string" && item.trim().startsWith("<") // If it's an HTML string, 125 | ? createDOMFromString(item, options) // create DOM elements from it 126 | : item instanceof HTMLElement // If it's already a DOM element 127 | ? [item] // return it as an array 128 | : item instanceof NodeList // If it's a NodeList 129 | ? Array.from(item) // return it as an array 130 | : options.all // If the all flag is set 131 | ? Array.from(document.querySelectorAll(item)) // return all matching elements from the DOM as an array 132 | : [document.querySelector(item)] // Otherwise, return the first matching element from the DOM as an array 133 | } 134 | 135 | export function createDOMFromString(string, { sanitize = true, sanitizer }) { 136 | const div = document.createElement("div") 137 | sanitize ? div.setHTML(string, sanitizer) : (div.innerHTML = string) 138 | return Array.from(div.children) 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jessquery 2 | 3 | `jessquery` is a lightweight wrapper around the DOM API that offers the intuitive elegance of jQuery, but streamlined for the modern web. 4 | 5 | Feel like a 🦕 for still using jQuery? Wish that it didn't bloat up your bundle size like a 🐖? Want something 🆕 and ✨? 6 | 7 | Rekindle your love for method chaining-- now in a lightweight, type-safe package! With **43** custom methods that are sneakily powerful, `jessquery` helps you seamlessly handle asynchronous tasks, customize error behaviors, and ensure your DOM operations always execute in order. And, the best part? 🏎️💨 8 | 9 | | Library | Size before gzip | Size after gzip | 10 | | --------- | ---------------- | --------------- | 11 | | jQuery | 88.3kb | 31.7kb | 12 | | jessquery | 8.82kb | 3.76kb | 13 | 14 | And, if that's too big for you, you can use our scrawny kid brother [Droxy](https://github.com/jazzypants1989/droxy/) instead. He's only 2kb after gzip! 15 | 16 | ![It's only 3.76kb! I swear! This badge proves it.](https://deno.bundlejs.com/badge?q=jessquery@2.4.3) 17 | [![npm version](https://badge.fury.io/js/jessquery.svg)](https://badge.fury.io/js/jessquery) 18 | 19 | - [Basic Usage](#basic-usage) 20 | - [Installation](#installation) 21 | - [Demo and Key Concepts](#demo-and-key-concepts) 22 | - [TypeScript](#typescript) 23 | - [The Rules](#the-rules) 24 | - [Code Walkthrough](#code-walkthrough) 25 | - [Advanced Usage](#advanced-usage) 26 | - [Interfaces](#interfaces) 27 | - [Contributing](#contributing) 28 | 29 | ## Basic Usage 30 | 31 | ```javascript 32 | // Most things follow the DOM API closely with slightly different names. 33 | // But, now you can chain them together 34 | // They will always execute in order! 35 | const fadeIn = [{ opacity: 0 }, { opacity: 1 }] // WAAPI keyframes 36 | const fadeOut = [{ opacity: 1 }, { opacity: 0 }] // WAAPI keyframes 37 | const animatedText = $$(".animated-text") // $$ ≈ querySelectorAll, use $ for querySelector 38 | 39 | // 40 | // 41 | animatedText 42 | .addClass("special") 43 | .wait(1000) // Will not appear for one second 44 | .toggle("hidden") 45 | .text( 46 | `

47 | In two seconds, every element matching the 'animated-text' class 48 | will fade in and out twice then disappear. 49 |

` 50 | ) 51 | .wait(2000) 52 | .transition(fadeIn, 1000) 53 | .transition(fadeOut, 1000) 54 | .transition(fadeIn, 1000) 55 | .transition(fadeOut, 1000) 56 | .purge() // All `.animated-text` elements will be removed from the DOM 57 | ``` 58 | 59 | ## Installation 60 | 61 | You can install it via NPM, PNPM, Yarn, or Bun just like anything else on NPM. 62 | 63 | ```bash 64 | npm install jessquery 65 | pnpm install jessquery 66 | yarn add jessquery 67 | bun install jessquery 68 | ``` 69 | 70 | Or, since it's so small, you can just use a CDN like the good, old days. The big problem with this is that you lose the types and the JSDoc annotations. I keep those in the `d.ts` file to keep the file size small, but I recently learned that gzip takes care of that for you. So, I'll probably change that in the future. For now, you can just use the `index.d.ts` file in your project if you want the types without installing the package. 71 | 72 | ```html 73 | 74 | 75 | ``` 76 | 77 | ## Demo and Key Concepts 78 | 79 | `jessquery` is quite different from jQuery, but it makes sense once you understand the rules. The concurrent chaining makes things a bit more complex. The key is understanding that each `$()` or `$$()` call is representative of a single queue-- not necessarily the elements that are being manipulated. It's a bit like [PrototypeJS](http://prototypejs.org/doc/latest/dom/dollar-dollar/) mixed with the async flow of something like [RxJS](https://rxjs.dev/guide/overview). 80 | 81 | The magic sauce here is that everything is a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), so you can still use the full DOM API if your use case isn't covered by one of the methods. So, if you forget about the `.css` operator and use `.style` instead when using `$()`, it will just work. The NodeList that you get from `$$()` is automatically turned into an array so you can use array methods on it like `.map()` or `.filter()`. 82 | 83 | This is the benefit of using proxies, but I'm curious if this will scale well as they bring a tiny bit of overhead. This might get problematic in large applications, but I'm probably just being paranoid. I welcome anyone to do some tests! 😅 84 | 85 | Here's a [Stackblitz Playground](https://stackblitz.com/edit/jessquery?file=main.js) if you want to try it out. The demo that will load in has an extremely long chain showing the mutability that a standard `DomProxy` exhibits. To see how an error is thrown when that proxy is `fixed` in place, simply add a `true` argument to the `$()` call like this: `const container = $(".container", true)`. 86 | 87 | ## TypeScript 88 | 89 | Everything is fully type-safe, but there's no way for the `$()` and `$$()` functions to infer the type of the element you're selecting unless it's a tag name. Things like `$$('input')` will always be fully inferred even if you map over the individual elements in the collection-- in that case, each element would automatically become an `HTMLInputElement`. However, if you select a class or id, the type will always be `HTMLElement` unless you specify the type yourself like this: 90 | 91 | ```typescript 92 | const button = $(".button") 93 | 94 | const coolInputs = $$(".cool-inputs") 95 | ``` 96 | 97 | ## The Rules 98 | 99 | I wrote a lot, but the main idea is that everything should be predictable. You probably only need to read the bold parts unless you start doing a lot of crazy DOM manipulation that operates on multiple elements at once while using the same variables for everything. If you're just doing simple stuff, you can probably just ignore the rest. 👌 100 | 101 | 1. **Use `$()` to build a queue that operates on a single element-- a DomProxy**. However, if you use a method like `pickAll()` or `kids()`, you will switch to a `DomProxyCollection` with multiple elements. 102 | 2. **Use `$$()` to build a queue that operates on multiple elements at once-- a DomProxyCollection**. However, if you use a method like `pick()` or `parent()` and there is only one element in the collection, you will switch to a `DomProxy` with a single element. 103 | 3. **Every `DomProxy` is mutable unless it was created with a `fixed` argument set to `true`**. If you store it in a variable and you change the element with a method like `next()` or `siblings()`, any event handlers that use that variable for DOM manipulation will now operate on the new element unless you use the `refresh()` method to reset the proxy to its original state. 104 | 4. _ALL_ `jessquery` custom methods can be chained together. Each method will operate on the element(s) held in the proxy at the time the function is called. If you switch context multiple times, it can get confusing. **Try to only switch "element context" once per method chain**. If you do not want your proxy to be mutable, set the `fixed` argument to true. 105 | 5. **_ALL_ `jessquery` custom methods are setters that return the proxy**. If you need to check the value of something, just use the DOM API directly (`textContent` instead of `text()`, for example). This also helps to differentiate between set and get operations. 106 | 6. **_ALL_ DOM API's can be used, but they MUST COME LAST within a single chain**. You can always start a new chain if you need to. You can even use the same variable-- you just need to know that function won't be executed until the previous chain finishes or hits a microtask. 107 | 7. **Each variable tied to a single `$()` or `$$()` call gets its own queue which runs every function sequentially, but the chains are executed concurrently**. So, you can have multiple chains operating on the same element. Be careful of race conditions! 108 | 8. All chains are begun in the order they are found in the script, but they await any microtasks or promises found before continuing. If you need to do things concurrently, **just make a new variable so you get a new queue**. 109 | 9. **Synchronous tasks are always executed as soon as possible, but not until their turn is reached in the queue.** If they are preceded by an async task, they will be added to the queue, executed in order, and any promises in their arguments will be awaited before the function is called. 110 | 10. **Each method is blocking, so if you use the same variable for event handlers, you will block the event handler from firing until that function is finished**. This is particularly problematic if that chain has any `wait()` calls or long animations. 111 | 112 | Generally, just try to keep each discrete chain of DOM operations for a single element together, and try to use a new variable for any event handlers. I mean, the whole point of this library is that `$()` and `$$()` are really easy to type, and you only need to worry about it when things aren't behaving the way you expect. If anything gets too hard, you can also use the `defer()` and `wait()` methods to let the DOM catch up while you re-evaluate your life choices. 😅 113 | 114 | ## Code Walkthrough 115 | 116 | [![I recorded a three hour long video explaining the code.](https://i.ytimg.com/vi/BVKHJcd4Ifs/maxresdefault.jpg)](https://www.youtube.com/watch?v=BVKHJcd4Ifs "I recorded a three hour long video explaining the code.") 117 | 118 | ## Advanced Usage 119 | 120 | - [DomProxy Lifecycle](#domproxy-lifecycle) 121 | - [Conditional Logic](#conditional-logic) 122 | - [Async Flow](#async-flow) 123 | - [Helper Functions: `promisify` and `setErrorHandler`](#helper-functions-promisify-and-seterrorhandler) 124 | - [AJAX](#ajax) 125 | 126 | ### DomProxy Lifecycle 127 | 128 | - While you can use `$()` and `$$()` as direct replacements for `querySelector` and `querySelectorAll`, you can also use them to create elements on the fly. 129 | 130 | - If you pass a string that starts with `<`, it will create a new element with the given tag name and return it as a DomProxy. 131 | 132 | - You can also pass an HTMLElement or NodeList, which will be converted to a DomProxyCollection. 133 | 134 | - Once the proxy has been created, it can be mutated to represent something else in the DOM. 135 | 136 | - If you use a method like `next()` or `siblings()`, the proxy will represent those elements instead. 137 | 138 | - The `refresh()` method will reset the proxy to its original state (What you passed to `$()` or `$$()`). 139 | 140 | ```javascript 141 | import { $, $$, promisify, setErrorHandler } from "jessquery" 142 | 143 | // Single element. 144 | const display = $(".display") 145 | 146 | // Multiple elements. 147 | const dynamicSpans = $$(".dynamic-spans") 148 | 149 | // Dynamically create an element. 150 | $(`

I'm #1

`).moveTo(".container", { position: "prepend" }) // append is the default 151 | 152 | // You can completely replace the element(s). The proxy is what's stable-- not the element(s) inside. 153 | // These changes are permanent as they operate on the DOM directly. 154 | const button = $(".button") 155 | button.html(`

I am not a button

`, true) 156 | // The second argument determines if the parent element should be replaced as well. 157 | 158 | // You can also use become() to transform the elements into something from elsewhere in the DOM. 159 | const buttons = $(".button") 160 | buttons.become($("#other-button")) 161 | // Each button will now be a deep clone of the other-button element. (no event handlers) 162 | 163 | // moveTo, cloneTo, and attach allow you to move elements around the DOM. 164 | // If you don't want to move things permanently, you can: 165 | // - use cloneTo to make a copy of the current element(s) and attach it to another element 166 | // - set the mode to `clone` on attach to copy another element from elsewhere in the DOM 167 | // However, this will not be able to move event handlers assigned to the original element(s). 168 | const coolDiv = $(".cool-div") 169 | coolDiv.cloneTo(".container", { position: "prepend" }) 170 | 171 | // But remember, you can use the same proxy to operate on different elements over time. 172 | coolDiv.next().text("I'm the next element!") 173 | // The coolDiv proxy will no longer operate on elements with the class "cool-div" 174 | 175 | // When in doubt, just use refresh() to reset the proxy to its original state. 176 | // /** 177 | // *
178 | // *

1

179 | // *

2

180 | // *

3

181 | // *
182 | // */ 183 | const ME = $("#ME") 184 | ME.do((el) => console.log(el.textContent)) // 1 185 | ME.next().do((el) => console.log(el.textContent)) // 2 186 | ME.parent().do((el) => console.log(el.textContent)) // 123 187 | ME.refresh().do((el) => console.log(el.textContent)) // 1 188 | ``` 189 | 190 | ### Conditional Logic 191 | 192 | - Usually, your best move is to simply use whatever DOM API you need to check the state of the element(s) in the proxy. 193 | 194 | - `if` and `takeWhile` allow you to conditionally execute functions in the middle of a chain. 195 | 196 | - They use a predicate function that returns a boolean to determine their behavior. 197 | 198 | - `if` is used for conditionally executing functions based on the state of the element(s) in the proxy. 199 | 200 | - `takeWhile` is used for conditionally filtering the elements held within the proxy. 201 | 202 | ```javascript 203 | const button = $(".button") 204 | const display = $(".display") 205 | 206 | // My first suggestion is to use a simple ternary with the DOM API. 207 | button.on("click", () => { 208 | display.text(display.textContent === "Click me!" ? "Clicked!" : "Click me!") 209 | }) 210 | 211 | // But, the if method allows you to chain things together. 212 | button.on("click", () => { 213 | display 214 | .if({ 215 | is: (el) => el.textContent === "Click me!", 216 | then: (el) => el.text("Clicked!").css("color", "green"), 217 | or: (el) => el.text("Click me!").css("color", "red"), 218 | }) 219 | .wait(1000) 220 | .if({ 221 | is: (el) => el.textContent === "Clicked!", 222 | then: (el) => el.text("Click me!").css("color", "red"), 223 | or: (el) => el.text("Clicked!").css("color", "green"), 224 | }) 225 | }) 226 | 227 | // `takeWhile` actually changes the elements held within the proxy. 228 | const buttons = $$(".button") 229 | 230 | buttons 231 | .takeWhile((el) => el.textContent !== "Click me!") 232 | .css("color", "red") // Only the buttons that don't say "Click me!" will be red. 233 | .wait(1000) 234 | .css("color", "blue") // Those buttons will turn blue after one second. 235 | ``` 236 | 237 | - It's important to remember that `takeWhile` will alter the elements held within the proxy, and that any methods that follow it will **only** operate on the filtered elements. 238 | 239 | - As always, you can use the `refresh` method to restore the proxy to its original state. 240 | 241 | ```javascript 242 | const buttons = $$(".button") 243 | 244 | buttons 245 | .takeWhile((el) => el.textContent !== "Click me!") 246 | .css("color", "red") // Only the buttons that don't say "Click me!" will have red text. 247 | .refresh() 248 | .css("background-color", "blue") // All buttons will be blue. 249 | ``` 250 | 251 | ### Async Flow 252 | 253 | - Because all promises are automatically awaited, you can create async functions to stick right in the middle of your chains. 254 | 255 | - No need to try/catch anything. All methods are surrounded by the error handler. 256 | 257 | ```javascript 258 | // You can do anything you want for however long you want. 259 | async function fetchData() { 260 | const response = await fetch("https://api.github.com/users/jazzypants1989") 261 | const data = await response.json() 262 | return data.name 263 | } 264 | 265 | // Every promise is resolved automatically 266 | button.on("click", () => { 267 | // The next function never runs until the previous one is finished. 268 | display 269 | .text(fetchData()) // No await, no try/catch, no problem! 270 | .css(color, display.textContent === "Jesse Pence" ? "green" : "red") 271 | // Each proxy has full access to the DOM API-- useful for conditional logic. 272 | 273 | display 274 | .do(async (el) => { 275 | // For more complex async logic, you can use the do method. 276 | // It will hold the queue indefinitely while you do whatever you want. 277 | el.text("Loading...") 278 | const response = await fetch( 279 | "https://api.github.com/users/jazzypants1989" 280 | ) 281 | const data = await response.json() 282 | el.text(data.name).css("color", "green") 283 | await new Promise((resolve) => setTimeout(resolve, 3000)) 284 | }) 285 | .text( 286 | "This will be green and replace the element's text, but only after three seconds of waiting!" 287 | ) 288 | }) 289 | ``` 290 | 291 | ### Helper Functions: `promisify` and `setErrorHandler` 292 | 293 | - promisify is a helper function that allows you to easily integrate asynchronous tasks into your chains. 294 | 295 | - It's particularly useful for things like setTimeout, setInterval, and any older APIs that use callbacks. 296 | 297 | - You can always just return a promise yourself if you want, but this provides a few extra features. 298 | 299 | - You can also use the `setErrorHandler` function to customize the behavior when an error occurs. 300 | 301 | ```javascript 302 | // It's great for when you forget to cover all conditions like this troubling example, 303 | // The next function will try to wait (five seconds by default). 304 | // If it still hasn't resolved, the chain will keep moving 305 | // (while passing an error to the error handler). 306 | const pollAPIUntilItWorks = promisify( 307 | (resolve, reject) => { 308 | const response = await fetch("https://someCrappyAPI.com") 309 | if (response.ok) { 310 | resolve(response.json()) // you can just pass this promise through 311 | } 312 | // No reject, no problem! It'll be covered by the default error handler. 313 | }, 314 | { 315 | timeout: 10000, 316 | // You can set the timeout to whatever you want. 317 | interval: 500, 318 | // It will usually only try the function once, but you can set an interval to retry. 319 | url: "https://someCrappyAPI.com", 320 | // You can pass extra metadata to the error handler. 321 | } 322 | ) 323 | 324 | // The default error handler catches most errors and promisify rejections 325 | // It simply logs using console.error, but you can use setErrorHandler to override this. 326 | setErrorHandler((err, context) => { 327 | sendErrorToAnalytics(err, context) 328 | }) 329 | 330 | display.on("mouseover", () => { 331 | display 332 | .html(`
JSON.stringify(${pollAPIUntilItWorks()}, null, 2)
`) 333 | .attach( 334 | `This will wait for ten seconds, but it will still show up if the API fails!` 335 | ) 336 | }) 337 | ``` 338 | 339 | ### AJAX 340 | 341 | - There's also internal `fromJSON`, `fromHTML`, and `fromStream` methods 342 | 343 | - (Above, I just wanted to show off the `promisify` method and how you don't have to await anything.) 344 | 345 | - These automatically handle fetching and parsing. 346 | 347 | - These functions expand fetch to incude additional event hooks and error handling. 348 | 349 | ```javascript 350 | const fetchOptions = { 351 | // You can use the same options as fetch, but with loads of extra features like: 352 | // Automatic Retries with exponential backoff. (default is 0) 353 | retries: 3, 354 | // The first retry will be in one second, then two seconds, then four seconds... 355 | retryDelay: 1000, 356 | // You can set up an error handler that will be called if the fetch still fails after all retries. 357 | onError: (err) => sendFetchErrorToAnalytics(err). 358 | // Or, a success handler that will reflect the DOM AFTER the element has been updated. 359 | onSuccess: () => dynamicSpans.attach("
Data Loaded!
"), 360 | // This custom loading message will replace the element's text while it waits. 361 | onWait: () => dynamicSpans.text("Hold your horses! I'm loading data!") 362 | // But, only if it doesn't load within one second. (default is 250ms and no message) 363 | waitTime: 1000, 364 | // Everything is sanitized by default, but you can turn it off if you want. 365 | runScripts: true, 366 | sanitize: false, 367 | // the full range of fetch options (request) are still supported. 368 | headers: { 369 | "Cool-Header": "Cool-Value", 370 | }, 371 | } 372 | 373 | // You can nest things as deep as you want (if you like making things confusing). 374 | dynamicSpans.fromJSON( 375 | "https://jessepence.com/api/cool-json", 376 | (el, json) => { 377 | el.html( 378 | `

${json.name}

379 |

${json.bio}

380 | ` 381 | ) 382 | .wait(5000) 383 | .fromHTML("/api/extended-bio", fetchOptions) 384 | .attach( 385 | "

Enough about me, I'll replace this with a cool stream in 5 seconds!

" 386 | ) 387 | .wait(5000) 388 | .fromStream("/api/cool-stream", fetchOptions) 389 | }, 390 | fetchOptions 391 | ) 392 | ``` 393 | 394 | - That covers most GET requests, but you can also use the `send` method for sending HTTP requests. 395 | 396 | - It automatically attempts to find any form data and a URL from the element or its parents. 397 | 398 | - It serializes using the formData API by default, but you can also pass a custom serializer. 399 | 400 | - You can also pass a URL and body directly if you want. 401 | 402 | ```javascript 403 | // This will automatically serialize the form 404 | // It will send it to the action attribute if it exists (The page's URL if not) 405 | $("#bigForm").send() 406 | 407 | // You can also customize each aspect of the request if you want. 408 | // EVERYTHING is optional. You can just pass a URL if you want. 409 | $("#otherSubmitButton").on("click", (event) => { 410 | $$("#bigForm").send({ 411 | // Just pass in the event to prevent default submission 412 | event, 413 | // If no URL, it will use the formaction attribute or any ancestor form's action that exists. 414 | url: "/api/cool-endpoint", 415 | // If no body, the form's data would be used. If no form, the value/textContent would be used. 416 | body: { cool: "data" }, 417 | // POST is the default 418 | method: "PUT", 419 | // You still get all the extra event hooks and options. 420 | onWait: () => console.log("Waiting for the server to respond..."), 421 | // And, of course, all the normal fetch options as well. 422 | headers: { 423 | "Cool-Header": "Cool-Value", 424 | }, 425 | }) 426 | }) 427 | // (this will send multiple fetches ($$) though. No caching or batching... yet) 428 | ``` 429 | 430 | ## Interfaces 431 | 432 | ### Table of Contents 433 | 434 | - [$()](#$) 435 | - [$$()](#$$) 436 | - [setErrorHandler](#setErrorHandler) 437 | - [promisify](#promisify) 438 | - [DomProxy](#DomProxy) 439 | - [DomProxy Methods](#DomProxy-Methods) 440 | - [DomProxy.on](#DomProxyon) 441 | - [DomProxy.once](#DomProxyonce) 442 | - [DomProxy.off](#DomProxyoff) 443 | - [DomProxy.delegate](#DomProxydelegate) 444 | - [DomProxy.html](#DomProxyhtml) 445 | - [DomProxy.sanitize](#DomProxysanitize) 446 | - [DomProxy.text](#DomProxytext) 447 | - [DomProxy.val](#DomProxyval) 448 | - [DomProxy.css](#DomProxycss) 449 | - [DomProxy.addStyleSheet](#DomProxyaddStyleSheet) 450 | - [DomProxy.addClass](#DomProxyaddClass) 451 | - [DomProxy.removeClass](#DomProxyremoveClass) 452 | - [DomProxy.toggleClass](#DomProxytoggleClass) 453 | - [DomProxy.set](#DomProxyset) 454 | - [DomProxy.unset](#DomProxyunset) 455 | - [DomProxy.toggle](#DomProxytoggle) 456 | - [DomProxy.data](#DomProxydata) 457 | - [DomProxy.attach](#DomProxyattach) 458 | - [DomProxy.cloneTo](#DomProxycloneTo) 459 | - [DomProxy.moveTo](#DomProxymoveTo) 460 | - [DomProxy.become](#DomProxybecome) 461 | - [DomProxy.purge](#DomProxypurge) 462 | - [DomProxy.send](#DomProxysend) 463 | - [DomProxy.do](#DomProxydo) 464 | - [DomProxy.defer](#DomProxydefer) 465 | - [DomProxy.fromJSON](#DomProxyfromJSON) 466 | - [DomProxy.fromHTML](#DomProxyfromHTML) 467 | - [DomProxy.fromStream](#DomProxyfromStream) 468 | - [DomProxy.transition](#DomProxytransition) 469 | - [DomProxy.wait](#DomProxywait) 470 | - [DomProxy.next](#DomProxynext) 471 | - [DomProxy.prev](#DomProxyprev) 472 | - [DomProxy.first](#DomProxyfirst) 473 | - [DomProxy.last](#DomProxylast) 474 | - [DomProxy.parent](#DomProxyparent) 475 | - [DomProxy.ancestor](#DomProxyancestor) 476 | - [DomProxy.pick](#DomProxypick) 477 | - [DomProxy.pickAll](#DomProxypickAll) 478 | - [DomProxy.siblings](#DomProxysiblings) 479 | - [DomProxy.kids](#DomProxykids) 480 | - [DomProxy.if](#DomProxyif) 481 | - [DomProxy.takeWhile](#DomProxytakeWhile) 482 | - [DomProxy.refresh](#DomProxyrefresh) 483 | - [DomProxyCollection](#DomProxyCollection) 484 | - [DomProxyCollection Methods](#DomProxyCollection-Methods) 485 | - [DomProxyCollection.on](#DomProxyCollectionon) 486 | - [DomProxyCollection.once](#DomProxyCollectiononce) 487 | - [DomProxyCollection.off](#DomProxyCollectionoff) 488 | - [DomProxyCollection.delegate](#DomProxyCollectiondelegate) 489 | - [DomProxyCollection.html](#DomProxyCollectionhtml) 490 | - [DomProxyCollection.sanitize](#DomProxyCollectionsanitize) 491 | - [DomProxyCollection.text](#DomProxyCollectiontext) 492 | - [DomProxyCollection.val](#DomProxyCollectionval) 493 | - [DomProxyCollection.css](#DomProxyCollectioncss) 494 | - [DomProxyCollection.addStyleSheet](#DomProxyCollectionaddStyleSheet) 495 | - [DomProxyCollection.addClass](#DomProxyCollectionaddClass) 496 | - [DomProxyCollection.removeClass](#DomProxyCollectionremoveClass) 497 | - [DomProxyCollection.toggleClass](#DomProxyCollectiontoggleClass) 498 | - [DomProxyCollection.set](#DomProxyCollectionset) 499 | - [DomProxyCollection.unset](#DomProxyCollectionunset) 500 | - [DomProxyCollection.toggle](#DomProxyCollectiontoggle) 501 | - [DomProxyCollection.data](#DomProxyCollectiondata) 502 | - [DomProxyCollection.attach](#DomProxyCollectionattach) 503 | - [DomProxyCollection.cloneTo](#DomProxyCollectioncloneTo) 504 | - [DomProxyCollection.moveTo](#DomProxyCollectionmoveTo) 505 | - [DomProxyCollection.become](#DomProxyCollectionbecome) 506 | - [DomProxyCollection.purge](#DomProxyCollectionpurge) 507 | - [DomProxyCollection.send](#DomProxyCollectionsend) 508 | - [DomProxyCollection.do](#DomProxyCollectiondo) 509 | - [DomProxyCollection.defer](#DomProxyCollectiondefer) 510 | - [DomProxyCollection.fromJSON](#DomProxyCollectionfromJSON) 511 | - [DomProxyCollection.fromHTML](#DomProxyCollectionfromHTML) 512 | - [DomProxyCollection.fromStream](#DomProxyCollectionfromStream) 513 | - [DomProxyCollection.transition](#DomProxyCollectiontransition) 514 | - [DomProxyCollection.wait](#DomProxyCollectionwait) 515 | - [DomProxyCollection.next](#DomProxyCollectionnext) 516 | - [DomProxyCollection.prev](#DomProxyCollectionprev) 517 | - [DomProxyCollection.first](#DomProxyCollectionfirst) 518 | - [DomProxyCollection.last](#DomProxyCollectionlast) 519 | - [DomProxyCollection.parent](#DomProxyCollectionparent) 520 | - [DomProxyCollection.ancestor](#DomProxyCollectionancestor) 521 | - [DomProxyCollection.pick](#DomProxyCollectionpick) 522 | - [DomProxyCollection.pickAll](#DomProxyCollectionpickAll) 523 | - [DomProxyCollection.siblings](#DomProxyCollectionsiblings) 524 | - [DomProxyCollection.kids](#DomProxyCollectionkids) 525 | - [DomProxyCollection.if](#DomProxyCollectionif) 526 | - [DomProxyCollection.takeWhile](#DomProxyCollectiontakeWhile) 527 | - [DomProxyCollection.refresh](#DomProxyCollectionrefresh) 528 | - [FetchOptions](#FetchOptions) 529 | 530 | ### $() 531 | 532 | - **$(selector: string, fixed?: boolean): DomProxy** 533 | 534 | - Finds the first element in the DOM that matches a CSS selector and returns it with some extra, useful methods. 535 | - If given a string that starts with `<`, it will create a new element with the given tag name and return it as a DomProxyCollection 536 | - It can also accept HTMLElements and NodeLists, which will be converted to a DomProxyCollection. 537 | - These contain 43 methods that can be chained together to create a sequence of actions that will be executed in order (including asynchronous tasks). 538 | - Every method returns a DomProxy or DomProxyCollection object, which can be used to continue the chain. 539 | - All DOM API's can still be used, but they _MUST COME LAST_ within a single chain. 540 | 541 | - Example: 542 | 543 | ```javascript 544 | $("#button") 545 | .on("click", () => console.log("Clicked!")) 546 | .css("color", "purple") 547 | .wait(1000) 548 | .css("color", "lightblue") 549 | .text("Click me!").style.backgroundColor = "lightGreen" // This will work, but only because it's the last thing in the chain. 550 | // It's also important to note that the style method call is not queued, so it will happen before everything else. 551 | ``` 552 | 553 | ### $$() 554 | 555 | - **$$(selector: string, fixed?: boolean): DomProxyCollection** 556 | 557 | - Finds all elements in the DOM that match a CSS selector and returns them with some extra, useful methods 558 | - If given a string that starts with `<`, it will create a new element with the given tag name and return it as a DomProxyCollection 559 | - It can also accept HTMLElements and NodeLists, which will be converted to a DomProxyCollection. 560 | - These contain 43 methods that can be chained together to create a sequence of actions that will be executed in order (including asynchronous tasks). 561 | - Every method returns a DomProxy or DomProxyCollection object, which can be used to continue the chain. 562 | - All DOM API's can still be used, but they _MUST COME LAST_ within a single chain. 563 | 564 | - Example: 565 | 566 | ```javascript 567 | $$(".buttons") 568 | .on("click", () => console.log("Clicked!")) 569 | .css("color", "purple") 570 | .wait(1000) 571 | .css("color", "lightblue") 572 | .text("Click me!").style.backgroundColor = "lightGreen" // This will work, but only because it's the last thing in the chain. 573 | // It's also important to note that the style method call is not queued, so it will happen before everything else. 574 | ``` 575 | 576 | ### setErrorHandler 577 | 578 | Sets an error handler that will be called when an error occurs somewhere in JessQuery. The default behavior is to just log it to the console. You can override this behavior with this method to do something else (or nothing... no judgement here! 😉) 579 | 580 | - **handler: (err: Error, context?: any) => void** 581 | 582 | - The error handler 583 | 584 | - Example: 585 | 586 | ```javascript 587 | setErrorHandler((err) => alert(err.message)) 588 | // Now, you'll get an annoying alert every time an error occurs like a good little developer 589 | ``` 590 | 591 | ### promisify 592 | 593 | - **promisify(fn: (...args: any[]) => void, meta?: { timeout?: number, interval?: number, [key: string]: any }): () => Promise** 594 | 595 | - Wraps a function in a promise, allowing easy integration into DomProxy chains.. This is particularly useful for things like setTimeout and any older APIs that use callbacks. 596 | 597 | - It accepts a function that works just like building a normal promise: call the resolve function when the function is successful, and call the reject function when it fails. The value that you pass will get passed to whatever method you use to consume the promise. 598 | 599 | - If the function does not call either resolve or reject within the specified timeout, the promise will reject with an error. Every promise that rejects inside of a promisified function will get routed through the default errorHandler (which you can set with the [setErrorHandler](#seterrorhandler) function). 600 | 601 | - You can set the amount of time to wait before rejecting the promise with the timeout property on the meta object. If you don't provide a timeout, it will default to 5000ms. 602 | 603 | - You can also set an interval to retry the function if it fails. This is useful for things like polling an API. If you don't provide an interval, it will default to 0ms (no retries). 604 | 605 | - The easiest way to use the function that you get from this method is call it to provide values to one of the `DomProxy` methods like text() or html(), but you can also use the [DomProxy.do](#domproxydo) / [DomProxyCollection.do](#domproxycollectiondo) method to execute the function and use the result on the element / elements represented by them. 606 | 607 | - You can include a meta object with any additional information you would like to pass to the error handler. This is useful as the function name will be anonymous for every promisified function, so you can use this to differentiate between them. 608 | 609 | - **fn: (...args: any[]) => void** 610 | 611 | - The function to promisify 612 | 613 | - **meta**: _optional_ 614 | 615 | - **meta.timeout?: number** 616 | 617 | - The number of milliseconds to wait before automatically rejecting the promise. If this is not provided, it will be set to 5000ms. 618 | 619 | - **meta.interval?: number** 620 | 621 | - The number of milliseconds to wait before retrying the function if it fails. The function will only run once by default. 622 | 623 | - Example: 624 | 625 | ```javascript 626 | const fetchApiData = promisify((resolve, reject) => { 627 | const xhr = new XMLHttpRequest() 628 | xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1") 629 | xhr.onload = () => resolve(xhr.responseText) 630 | xhr.onerror = () => reject(xhr.statusText) 631 | xhr.send() 632 | }, { 633 | timeout: 10000, 634 | fnName: 'fetchApiData (XHR)' 635 | url: 'https://jsonplaceholder.typicode.com/todos/1' 636 | }) 637 | 638 | setErrorHandler((err) => $("#display").text(err.message)) 639 | 640 | button.on("click", () => { 641 | display 642 | .text("Hold on! I'm about to use XHR") 643 | .wait(500) 644 | .do(async (el) => { 645 | const data = await fetchApiData() 646 | el.text(data) 647 | }) 648 | }) 649 | 650 | // Or remember, you can just pass it into the text method! 651 | button.on("click", async () => { 652 | display 653 | .text("I betcha don't even know what XHR is!") 654 | .wait(1000) 655 | .text(fetchApiData()) 656 | }) 657 | ``` 658 | 659 | ### DomProxy 660 | 661 | A proxy covering a single HTML element that allows you to chain methods sequentially (including asynchronous tasks) and then execute them one after the other. It includes **43** of these custom methods, but you can still use the full DOM API if you need to. 662 | 663 | #### DomProxy Methods 664 | 665 | ##### DomProxy.on 666 | 667 | **on(ev: string, fn: EventListenerOrEventListenerObject): DomProxy** 668 | 669 | - Adds an event listener to the element. 670 | - Example: `$('button').on('click', () => console.log('clicked'))` 671 | 672 | ##### DomProxy.once 673 | 674 | **once(ev: string, fn: EventListenerOrEventListenerObject): DomProxy** 675 | 676 | - Adds an event listener to the element that will only fire once. 677 | - Example: `$('button').once('click', () => console.log('clicked'))` 678 | 679 | ##### DomProxy.off 680 | 681 | - **off(ev: string, fn: EventListenerOrEventListenerObject): DomProxy** 682 | 683 | - Removes an event listener from the element. 684 | - Example: `$('button').off('click', clickHandler)` 685 | 686 | ##### DomProxy.delegate 687 | 688 | - **delegate(event: string, subSelector: string, handler: EventListenerOrEventListenerObject): DomProxy** 689 | 690 | - Delegates an event listener to the element. 691 | - Example: `$('.container').delegate('click', '.buttons', (e) => console.log('Button clicked'))` 692 | 693 | ##### DomProxy.html 694 | 695 | - **html(newHtml: string, outerHTML?: boolean): DomProxy** 696 | 697 | - Change the HTML of the element with an **UNSANITIZED** string of new HTML. This is useful if you want to add a script tag or something. If you want to sanitize the HTML, use `sanitize` instead. 698 | 699 | - By default, only the element's children will be replaced (innerHTML). If you want to replace the entire element (outerHTML), you can pass `true` as the second argument. 700 | - Example: `$('button').html('Click me!')` 701 | - Expectation: `` 702 | - Example: `$('button').html('Click me!', true)` 703 | - Expectation: `Click me!` (The button will be replaced with the span) 704 | 705 | ##### DomProxy.sanitize 706 | 707 | - **sanitize: (html: string, sanitizer?: (html: string) => string) => DomProxy** 708 | 709 | - Sanitizes a string of untusted HTML, then replaces the element with the new, freshly sanitized HTML. This helps protect you from XSS Attacks. It uses the setHTML API under the hood, so you can provide your own sanitizer if you want with a second argument. 710 | - Example: 711 | 712 | ```javascript 713 | const maliciousHTML = 714 | 'Safe Content' 715 | const customSanitizer = new Sanitizer({ 716 | allowElements: ["span"], 717 | }) 718 | $("button").sanitize(maliciousHTML, customSanitizer) 719 | // The button will only contain the 'Safe Content' span; 720 | // Any scripts (or other unwanted tags) will be removed. 721 | // Only span elements will be allowed. 722 | ``` 723 | 724 | - MDN Documentation: [setHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML) 725 | 726 | ##### DomProxy.text 727 | 728 | - **text(newText: string): DomProxy** 729 | 730 | - Changes the text of the element while retaining the tag. 731 | - Example: `$('button').text('Click me!')` 732 | 733 | ##### DomProxy.val 734 | 735 | - **val(newValue: string | number | (string | number)[] | FileList): DomProxy** 736 | 737 | - Changes the value of the element based on its type. For form elements such as inputs, textareas, and selects, the appropriate property (e.g., `value`, `checked`) will be adjusted. For other elements, the `textContent` property will be set. 738 | - Example: `$('input[type="text"]').val('New Value')` 739 | - Example: `$('input[type="checkbox"]').val(true)` 740 | - Example: `$('input[type="radio"]').val('radio1')` 741 | - Example: `$('input[type="file"]').val(myFileList)` 742 | - Example: `$('select[multiple]').val(['option1', 'option2'])` 743 | 744 | ##### DomProxy.css 745 | 746 | - **css(prop: string | Record, value?: string): DomProxy** 747 | 748 | - Adds one or more CSS Rules to the element. 749 | 750 | - If the first argument is an object, each key-value pair will be added as a CSS Rule. 751 | 752 | - If the first argument is a string, it will be treated as a CSS property and the second argument will be treated as its value. 753 | 754 | - Example: `$('button').css('color', 'red')` 755 | - Example: `$('button').css({ color: 'red', backgroundColor: 'blue' })` 756 | 757 | ##### DomProxy.addStyleSheet 758 | 759 | - **addStyleSheet(cssString: string): DomProxy** 760 | 761 | - Adds a stylesheet to the ENTIRE DOCUMENT (this is useful for things like :hover styles). Got an good idea for how to make this scoped to a single element? Open a PR! 762 | 763 | - This should be used only when you need to do something like set a pseudo-class on the fly. Otherwise, just write a real stylesheet. 764 | 765 | - Got a good idea for how to make this scoped to a single element? Open a PR! I was thinking something like the `@scope` rule being automatically inserted, but that's still only in Chromium browsers. 766 | 767 | - [Here's the explainer for @scope.](https://css.oddbird.net/scope/explainer/). 768 | 769 | -- Right now, every rule will be given an !important flag, so it will override any existing styles. This is drastic I know, but it's the only way to make this work if you're creating other inline styles. 770 | 771 | - Example: `$('button').addStyleSheet('button:hover { color: red; }')` 772 | 773 | ##### DomProxy.addClass 774 | 775 | - **addClass(className: string): DomProxy** 776 | 777 | - Add one or more classes to the element. 778 | 779 | - Just add a comma in between each class name, or use spread syntax from an array. 780 | 781 | - Example: `$('button').addClass('btn')` 782 | 783 | - Example: `$('button').addClass('btn', 'btn-primary')` 784 | 785 | - Example: 786 | 787 | ```javascript 788 | const classes = ["btn", "btn-primary"] 789 | $("button").addClass(...classes) 790 | ``` 791 | 792 | ##### DomProxy.removeClass 793 | 794 | - **removeClass(className: string): DomProxy** 795 | 796 | - Removes one or more classes from the element. 797 | 798 | - Just add a comma in between each class name, or use spread syntax from an array. 799 | 800 | - Example: `$('button').removeClass('btn')` 801 | 802 | - Example: `$('button').removeClass('btn', 'btn-primary')` 803 | 804 | - Example: 805 | 806 | ```javascript 807 | const classes = ["btn", "btn-primary"] 808 | $("button").removeClass(...classes) 809 | ``` 810 | 811 | ##### DomProxy.toggleClass 812 | 813 | - **toggleClass(className: string): DomProxy** 814 | 815 | - Toggles a class on the element. If given a second argument, it will add the class if the second argument is truthy, and remove it if the second argument is falsy. 816 | - Example: `$('button').toggleClass('btn')` 817 | - Example: 818 | 819 | ```javascript 820 | const mediumScreen = window.matchMedia("(min-width: 768px)").matches 821 | $("button").toggleClass("btn", mediumScreen) 822 | ``` 823 | 824 | ##### DomProxy.set 825 | 826 | - **set(attr: attr: string | { [key: string]: string },value?: string): DomProxy** 827 | 828 | - Sets one or more attributes on the element. 829 | 830 | - If the first argument is an object, each key-value pair will be treated as an attribute name and value. 831 | 832 | - If the first argument is a string, it will be treated as the attribute name and the second argument will be treated as the attribute value. 833 | 834 | - If the value is undefined, it will be set to `""`, which is useful for boolean attributes like disabled or hidden. 835 | 836 | - Example: `$('button').set('disabled')` 837 | 838 | - Example: `$('button').set('disabled', 'true')` 839 | 840 | - Example: `$('button').set({ disabled: 'true', hidden: '' })` 841 | 842 | ##### DomProxy.unset 843 | 844 | - **unset(attr: string): DomProxy** 845 | 846 | - Removes an attribute from the element. 847 | - Example: `$('button').unset('disabled')` 848 | 849 | ##### DomProxy.toggle 850 | 851 | - **toggle(attr: string): DomProxy** 852 | 853 | - Toggles an attribute on the element. If given a second argument, it will add the attribute if the second argument is truthy, and remove it if the second argument is falsy. 854 | - Example: `$('button').toggle('disabled')` 855 | 856 | - Example: 857 | 858 | ```javascript 859 | const mediumScreen = window.matchMedia("(min-width: 768px)").matches 860 | $("button").toggle("disabled", mediumScreen) 861 | ``` 862 | 863 | ##### DomProxy.data 864 | 865 | - **datakey: string | { [key: string]: string }, value?: string: DomProxy** 866 | 867 | - Sets one or more data attributes on the element. 868 | 869 | - If the first argument is an object, each key-value pair will be treated as a data attribute name and value. 870 | 871 | - If the first argument is a string, it will be treated as the data attribute name and the second argument will be treated as the data attribute value. 872 | 873 | - Example: `$('button').data('id', '123')` 874 | 875 | - Example: `$('button').data({ id: '123', cool: 'true' })` 876 | 877 | ##### DomProxy.attach 878 | 879 | - **attach(...children: (HTMLElement | DomProxy)[]): DomProxy** 880 | 881 | - Attaches children to the element based on the provided options. 882 | - The children can be: 883 | - A string of HTML 884 | - A CSS selector 885 | - An HTMLElement 886 | - A DomProxy 887 | - An array of any of the above 888 | - The position can be: 889 | - 'append' (default): Adds the children to the end of the element. 890 | - 'prepend': Adds the children to the beginning of the element. 891 | - 'before': Adds the children before the element. 892 | - 'after': Adds the children after the element. 893 | - The HTML is sanitized by default, which helps prevent XSS attacks. 894 | - If you want to disable sanitization, set the `sanitize` option to `false`. 895 | - Example: `$('button').attach('Click me!')` 896 | - Example: `$('button').attach($('.container'), { position: 'prepend' })` 897 | - Example: `$('button').attach([$('.container'), 'Click me!'], { position: 'before' })` 898 | - Example: `$('button').attach('')` // No XSS attack here! 899 | - Example: `$('button').attach('', { sanitize: false })` // XSS attack here! 900 | - [Handy StackOverflow Answer for Position Option](https://stackoverflow.com/questions/14846506/append-prepend-after-and-before) 901 | 902 | ##### DomProxy.cloneTo 903 | 904 | - **cloneTo(parentSelector: string, options?: { position: string; all: boolean }): DomProxy** 905 | 906 | - Clone of the element to a new parent element in the DOM. The original element remains in its current location. If you want to move the element instead of cloning it, use `moveTo`. 907 | 908 | - The position can be: 909 | 910 | - 'append' (default): Adds the children to the end of the parent. 911 | - 'prepend': Adds the children to the beginning of the parent. 912 | - 'before': Adds the children before the parent. 913 | - 'after': Adds the children after the parent. 914 | 915 | - The all option will clone the element into each new parent in the collection. If the all option is not passed, only the first parent in the collection will be used. 916 | 917 | - Example: `$('div').cloneTo('.target')` // Clones and places inside first .target element (default behavior) 918 | - Example: `$('div').cloneTo('.target', { position: 'after' })` // Clones and places after first .target element 919 | - Example: `$('div').cloneTo('.target', { all: true })` // Clones and places inside all .target elements 920 | - Example: `$('div').cloneTo('.target', { all: true, position: 'before' })` // Clones and places before all .target elements 921 | - [Handy StackOverflow Answer for Position Option](https://stackoverflow.com/questions/14846506/append-prepend-after-and-before) 922 | 923 | ##### DomProxy.moveTo 924 | 925 | - **moveTo(parentSelector: string, options?: { position: string }): DomProxy** 926 | 927 | - Move the element to a new parent element in the DOM. The original element is moved from its current location. If you want to clone the element instead of moving it, use `cloneTo`. 928 | 929 | - The position can be: 930 | 931 | - 'append' (default): Adds the children to the end of the parent. 932 | - 'prepend': Adds the children to the beginning of the parent. 933 | - 'before': Adds the children before the parent. 934 | - 'after': Adds the children after the parent. 935 | 936 | - The all option can technically be used, but it will only move the element to the last parent in the collection. This is because the element can only exist in one place at a time. Use `cloneTo` if you want to move the element to multiple parents. 937 | 938 | - Example: `$('div').moveTo('.target')` // Moves inside first .target element (default behavior) 939 | - Example: `$('div').moveTo('.target', { position: 'after' })` // Moves after first .target element 940 | - [Handy StackOverflow Answer for Position Option](https://stackoverflow.com/questions/14846506/append-prepend-after-and-before) 941 | 942 | ##### DomProxy.become 943 | 944 | - **become(replacement: HTMLElement | Array | DomProxy, options?: { mode?: "move" | "clone"; match?: "cycle" | "remove" }): DomProxy** 945 | 946 | - The become method is used to replace a single element with a different element from elsewhere in the DOM. 947 | - Under the hood, it utilizes the native `replaceWith` method but adds extra layers of functionality. 948 | - The replacement can be a simple HTMLElement, an array of HTMLElements, or another DomProxy instance. 949 | - Mode: 950 | - _clone_ (default) - This makes a copy of the replacement element to use for the DomProxy. This clone includes the element, its attributes, and all its child nodes, but does not include event listeners. The original element is left untouched. 951 | - _move_ - This moves the replacement element to the original element's position. The original element is removed from the DOM. This is the same as calling `replaceWith` directly. 952 | - Example: `$('div').become(newElement, {mode: "move"})` 953 | - Expectation: Replaces div with newElement, literally moving it to the original div's position. 954 | 955 | - Example: `$('div').become(newElement, {mode: "clone"})` 956 | - Expectation: Replaces div with a deep clone of newElement, leaving the original newElement untouched. 957 | 958 | - Example: `$('#button').become($$('.otherButtons'))` 959 | - Expectation: Takes another DomProxy as the replacement. The first element of the DomProxy is chosen for the replacement. 960 | 961 | ##### DomProxy.purge 962 | 963 | - **purge(): DomProxy** 964 | 965 | - Removes the element from the DOM entirely. 966 | - Example: `$('button').purge()` 967 | 968 | ##### DomProxy.send 969 | 970 | - **send(options: {url?: string, json?: boolean, event?: Event, serializer?: (element) => void} & FetchOptions): DomProxy** 971 | 972 | - Sends an HTTP request using the current element as the body of the request unless otherwise specified. 973 | - None of the options are required-- not even the URL. 974 | 975 | - If you do not provide a URL the method will: 976 | 977 | - First, look to see if it's in a form with an action property. 978 | - Next, it will look to see if the element is a button with a formaction property. 979 | - Next, it will try to see if the element is part of a form that has an action property. 980 | - Finally, it will take the current URL and slice off everything after the last slash. (http://example.com/foo/index.html -> http://example.com/foo/) 981 | 982 | - Unless the `body` option is provided, it will be created automatically based on the element type: 983 | 984 | - If it's a form, the entire form will be serialized using the formData API unless a custom serializer is provided. 985 | - If it's an input, textarea, or select, the value will be used. 986 | - If it isn't a form or one of the above elements, we will check to see if the element has a form property or one can be found with the `closest` method. If so, we will serialize the form using the formData API unless a custom serializer is provided. 987 | - If none of the above, the element's textContent will be used. 988 | 989 | - If the `json` option is set to true, the request will be sent as JSON and the response will be parsed as JSON. 990 | - Otherwise, the request will be sent as FormData and the response will be parsed as text. 991 | 992 | - The serializer option can be used to provide a custom function to serialize the form. It will be passed the element as an argument and should return the serialized form data. 993 | 994 | - This will probably break if you don't return a FormData object, but I haven't tested it. 995 | 996 | - Example: 997 | 998 | ```javascript 999 | // Send a JSON request using input value, providing fallback and completion messages. 1000 | $("#myInput").send(myElement, { 1001 | url: "/api/data", 1002 | json: true, 1003 | onWait: () => $("#myInput").html('Loading...'), 1004 | waitTime: 500 // The onWait function only gets called if the request takes longer than 500ms 1005 | onSuccess: (data) => console.log("Received:", data), 1006 | onError: (error) => console.log("Error occurred:", error), 1007 | }) 1008 | ``` 1009 | 1010 | - Example: 1011 | 1012 | ```javascript 1013 | // Send form data using a custom serializer. 1014 | $("#myForm").send(myElement, { 1015 | url: "/api/data", 1016 | serializer: (form) => { 1017 | const formData = new FormData(form) 1018 | formData.append("extra", "data") 1019 | return formData 1020 | }, 1021 | }) 1022 | ``` 1023 | 1024 | ##### DomProxy.do 1025 | 1026 | - **do(fn: (el: DomProxy) => Promise): DomProxy** 1027 | 1028 | - Executes an asynchronous function and waits for it to resolve before continuing the chain (can be synchronous too). 1029 | - Can receive the element as an argument, and you can still use all the proxy methods inside the function. 1030 | 1031 | - Example: 1032 | 1033 | ```javascript 1034 | $("button").do(async (el) => { 1035 | const response = await fetch("https://api.github.com/users/jazzypants1989") 1036 | const data = await response.json() 1037 | el.text(data.name) 1038 | }) 1039 | ``` 1040 | 1041 | ##### DomProxy.defer 1042 | 1043 | - **defer(fn: (el: DomProxy) => void): DomProxy** 1044 | 1045 | - Schedules a function for deferred execution on the element. 1046 | 1047 | - This will push the operation to the very end of the internal event loop. 1048 | 1049 | - Usually, everything will happen in sequence anyways. Given the predictability of each queue, `defer` has limited use cases and should be used sparingly. The whole point of JessQuery is to make things predictable, so you should just put the function at the end of the chain if you can. 1050 | 1051 | - The only problem is if you set up an event listener using the same variable that has lots of queued behavior-- especially calls to the wait method. Just wrap the wait call and everything after it in defer to ensure that event handlers don't get stuck behind these in the queue. 1052 | 1053 | - `defer` will capture the element at the time of the call, so this should not be mixed with context switching methods like `parent` or `pickAll`. 1054 | 1055 | - Honestly, I'm not sure if this even makes much sense. I just spent a bunch of time building a crazy queue system, and I feel like I need to expose it. If you have any ideas for how to make this more useful, please open an issue or PR. 1056 | 1057 | - Example: 1058 | 1059 | ```javascript 1060 | const button = $("button") 1061 | 1062 | button 1063 | .text("this won't do anything for a second because of the wait call") 1064 | .on("click", () => button.text("clicked")) 1065 | .wait(1000) 1066 | 1067 | //but if we wrap the wait call in defer, the events will not be queued behind it 1068 | button 1069 | .text("this will be immediately responsive due to the defer call") 1070 | .on("click", () => button.text("clicked")) 1071 | .defer((el) => el.wait(1000).text("Yay!")) 1072 | 1073 | // THIS ONLY OCCURS BECAUSE THE SAME VARIABLE IS USED FOR THE EVENT LISTENER AND THE CHAIN 1074 | $("button") 1075 | .on("click", () => $("button").text("clicked")) 1076 | .wait(1000) // NO PROBLEM HERE 1077 | ``` 1078 | 1079 | ##### DomProxy.fromJSON 1080 | 1081 | - **fromJSON(url: string, transformFunc: (el: DomProxy, json: any) => void, options?: FetchOptions): DomProxy** 1082 | 1083 | - Fetches a JSON resource from the provided URL and applies a transformation function which uses the fetched JSON and the proxy's target element as arguments. 1084 | - The transform function can be used to set the text, html, or any other property of the element. 1085 | - The options object can be used define a plethora of options defined in [FetchOptions](#FetchOptions). 1086 | - These options extend the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) interface, so you can use all of the normal fetch options as well. (e.g., method, headers, etc.) 1087 | - Example: 1088 | 1089 | ```javascript 1090 | $("#item").fromJSON("/api/data", (element, json) => { 1091 | element.text(json.value) 1092 | }) 1093 | ``` 1094 | 1095 | - Example: 1096 | 1097 | ```javascript 1098 | $("#item").fromJSON("/api/data", (element, json) => { 1099 | element.html(`${json.description}`) 1100 | }) 1101 | ``` 1102 | 1103 | - Example: 1104 | 1105 | ```javascript 1106 | $('#news-item').fromJSON('/api/news-item', (element, json) => { 1107 | { title, summary } = json; 1108 | 1109 | element.html(`

${title}

1110 |

${summary}

`); 1111 | }, 1112 | { 1113 | error: 'Failed to load news item', 1114 | pnWait: () => $('#news-item').text('Loading news item...'), 1115 | onSuccess: () => console.log('News item loaded') 1116 | } 1117 | ) 1118 | ``` 1119 | 1120 | ##### DomProxy.fromHTML 1121 | 1122 | - **fromHTML(url: string, options?: FetchOptions): DomProxy** 1123 | 1124 | - Fetches an HTML resource from the provided URL and inserts it into the proxy's target element. 1125 | - The options object can be used define a plethora of options defined in [FetchOptions](#FetchOptions). 1126 | - These options extend the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) interface, so you can use all of the normal fetch options as well. (e.g., method, headers, etc.) 1127 | - The HTML is sanitized by default, which helps prevent XSS attacks. 1128 | - Example: `$('#template').fromHTML('/template.html')` 1129 | - Example: `$('#update').fromHTML('/update.html', { onWait: () => $('#fromHTML)'.text('Loading update...'), error: 'Failed to load update!' })` 1130 | - Example: `$('#content').fromHTML('/malicious-content.html', { sanitize: false })` 1131 | 1132 | ##### DomProxy.fromStream 1133 | 1134 | - **fromStream(url: string, options?: { sse?: boolean; add?: boolean; toTop?: boolean; error?: string; onError?: () => void; onWait?: () => void; waitTime?: number; runScripts?: boolean; sanitize?: boolean; onSuccess?: (data: any) => void }): DomProxy** 1135 | 1136 | - Dynamically fetches data from the provided URL and updates a single DOM element using a stream or Server-Sent Event (SSE). 1137 | - This includes all the same options as the `fromHTML` and `fromJSON` methods for normal streams, but with a few caveats for SSEs. 1138 | - the `onSuccess` callback will be invoked after each message is received, and it will receive the last event as an argument. 1139 | - SSE's have two additional options, `add` and `onTop`, which determine how the new data is added to the existing content. 1140 | - The options object can be used define a plethora of options defined in [FetchOptions](#FetchOptions). 1141 | - These options extend the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) interface, so you can use all of the normal fetch options as well. (e.g., method, headers, etc.) 1142 | - The HTML is sanitized by default, which helps prevent XSS attacks. 1143 | - Example: `$('#content').fromStream('/api/data', { sanitize: false })` <-- Only for trusted sources! 1144 | - Example: `$('#liveFeed').fromStream('/api/live', { sse: true, add: true, onSuccess: (data) => console.log('New data received:', data) })` 1145 | 1146 | ##### DomProxy.transition 1147 | 1148 | - **transition(keyframes: Keyframe[] | PropertyIndexedKeyframes, options: KeyframeAnimationOptions): DomProxy** 1149 | 1150 | - Animates the element using the WAAPI. 1151 | - Returns the proxy so you can continue chaining. If you need to return the animation object, use the `animate` method instead. 1152 | - Remember, this method is blocking, so watch out for any event handlers using the same variable. 1153 | - Example: `$('button').transition([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 })` 1154 | - [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate) 1155 | 1156 | ##### DomProxy.wait 1157 | 1158 | - **wait(ms: number): DomProxy** 1159 | 1160 | - Waits for a specified number of milliseconds before continuing the chain. 1161 | - Returns the proxy so you can continue chaining. If you need to return the animation object, use the `animate` method instead. 1162 | - Remember, this method is blocking, so watch out for any event handlers using the same variable. 1163 | - If you do not want to switch to a new variable, make sure that you use this in combination with `defer` which will allow the events to skip ahead in the queue. 1164 | - `wait` will **always** reliably hold up a queue when used in the middle of a chain, but each method that contains the element as an argument will operate on a different queue so `wait()` calls inside of them will not be respected in subsequent methods on the parent chain. 1165 | - These methods, such as `do`, `fromJSON`, and `if`, will respect `wait` calls held within them for their own individual queues, but they will not hold up the queue of the parent chain. 1166 | - So, if you call `wait` inside the callback for the method, you must continue the chain inside. Otherwise, the chain will continue immediately. 1167 | - Example: `$('button').wait(1000)` 1168 | - Example: 1169 | 1170 | ```javascript 1171 | testButton 1172 | .on("click", () => { 1173 | testButton.text(`Clicked ${++count} times`) 1174 | }) 1175 | .wait(2000) 1176 | .css({ color: "red" }) // BAD! The click handler will not register for 2 seconds! 1177 | 1178 | testButton 1179 | .on("click", () => { 1180 | testButton.text(`Clicked ${++count} times`) 1181 | }) 1182 | .defer((el) => el.wait(2000).css({ color: "red" })) // Good! The click handler will register immediately! 1183 | 1184 | testButton 1185 | .on("click", () => { 1186 | $(".testButton").text(`Clicked ${++count} times`) 1187 | }) 1188 | .wait(2000) 1189 | .css({ color: "red" }) // Less good, but still better! 1190 | // You made a new proxy for no reason, but at least the user can click the button! 1191 | 1192 | testButton 1193 | .do((el) => el.wait(2000).css({ color: "green" }).wait(2000)) 1194 | .css({ color: "red" }) 1195 | // This will turn red, then green. NOT GREEN, THEN RED! 1196 | ``` 1197 | 1198 | ##### DomProxy.next 1199 | 1200 | - **next(): DomProxy** 1201 | 1202 | - Switch to the next sibling of the element in the middle of a chain. 1203 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1204 | - Example: `$('button').css('color', 'red').next().css('color', 'blue')` 1205 | - Expectation: The next sibling of the button will turn blue. The button itself will remain red. 1206 | 1207 | ##### DomProxy.prev 1208 | 1209 | - **prev(): DomProxy** 1210 | 1211 | - Switch to the previous sibling of the element in the middle of a chain. 1212 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1213 | - Example: `$('button').css('color', 'red').prev().css('color', 'blue')` 1214 | - Expectation: The previous sibling of the button will turn blue. The button itself will remain red. 1215 | 1216 | ##### DomProxy.first 1217 | 1218 | - **first(): DomProxy** 1219 | 1220 | - Switch to the first child of the element in the middle of a chain. 1221 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1222 | - Example: `$('button').css('color', 'red').first().css('color', 'blue')` 1223 | - Expectation: The first child of the button will turn blue. The button itself will remain red. 1224 | 1225 | ##### DomProxy.last 1226 | 1227 | - **last(): DomProxy** 1228 | 1229 | - Switch to the last child of the element in the middle of a chain. 1230 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1231 | - Example: `$('button').css('color', 'red').last().css('color', 'blue')` 1232 | - Expectation: The last child of the button will turn blue. The button itself will remain red. 1233 | 1234 | ##### DomProxy.parent 1235 | 1236 | - **parent(): DomProxy** 1237 | 1238 | - Switch to the parent of the element in the middle of a chain. 1239 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1240 | - Example: `$('button').css('color', 'red').parent().css('color', 'blue')` 1241 | - Expectation: The parent of the button will turn blue. The button itself will remain red. 1242 | 1243 | ##### DomProxy.ancestor 1244 | 1245 | - **ancestor(ancestorSelector: string): DomProxy** 1246 | 1247 | - Gets the closest ancestor matching a selector. Uses the `closest` API under the hood. 1248 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1249 | - Example: `$('.buttons').css('color', 'red').ancestor('.container').css('color', 'blue')` 1250 | - Expectation: The container will turn blue. The buttons will remain red. 1251 | 1252 | ##### DomProxy.pick 1253 | 1254 | - **pick(subSelector: string): DomProxy** 1255 | 1256 | - Gets the first descendant matching a sub-selector. 1257 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1258 | - Example: `$('.container').css('color', 'red').pick('.buttons').css('color', 'blue')` 1259 | - Expectation: The descendants of the container will turn blue. The container itself will remain red. 1260 | 1261 | ##### DomProxy.pickAll 1262 | 1263 | - **pickAll(subSelector: string): DomProxyCollection** 1264 | 1265 | - Gets all descendants matching a sub-selector. 1266 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1267 | - Example: `$('.container').css('color', 'red').pickAll('.buttons').css('color', 'blue')` 1268 | - Expectation: The descendants of the container will turn blue. The container itself will remain red. 1269 | 1270 | ##### DomProxy.siblings 1271 | 1272 | - **siblings(): DomProxyCollection** 1273 | 1274 | - Switch to the siblings of the element in the middle of a chain. 1275 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1276 | - Example: `$('button').css('color', 'red').siblings().css('color', 'blue')` 1277 | - Expectation: The siblings of the button will turn blue. The button itself will remain red. 1278 | 1279 | ##### DomProxy.kids 1280 | 1281 | - **kids(): DomProxyCollection** 1282 | 1283 | - Switch to the children of the element in the middle of a chain. 1284 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1285 | - Example: `$('button').css('color', 'red').kids().css('color', 'blue')` 1286 | - Expectation: The children of the button will turn blue. The button itself will remain red. 1287 | 1288 | ##### DomProxy.if 1289 | 1290 | - **if: (options: { is: (el: DomProxy) => boolean, then?: (el: DomProxy) => void, or?: (el: DomProxy) => void }) => DomProxy** 1291 | 1292 | - Executes conditional logic on the element based on its current state. 1293 | - The `is` option is a function that receives the element as an argument and returns a boolean. 1294 | - The `then` option is a function that receives the element as an argument and is executed if the `is` function returns true. 1295 | - The `or` option is a function that receives the element as an argument and is executed if the `is` function returns false. 1296 | 1297 | - Example: 1298 | 1299 | ```javascript 1300 | $("button").if({ 1301 | is: (el) => el.hasClass("active"), 1302 | then: (el) => el.text("Active"), 1303 | or: (el) => el.text("Inactive"), 1304 | }) 1305 | ``` 1306 | 1307 | ##### DomProxy.takeWhile 1308 | 1309 | - **takeWhile: (predicate: (el: DomProxy, reverse: boolean) => boolean) => DomProxy** 1310 | 1311 | - Filters the current element in the proxy based on a predicate. 1312 | - If the current element does not satisfy the predicate, the proxy will be emptied. 1313 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1314 | - The predicate is a function that receives the element as an argument and returns a boolean. 1315 | - The function will run from the first element to the last, unless you pass a second argument of true. Then, it will run from the last element to the first. This generally corresponds with position in the DOM, but it's not guaranteed. 1316 | - It's essential to use this method with caution as it can empty the proxy if the current element does not match the predicate. 1317 | - The `refresh()` method can be used to restore the proxy to its original state. 1318 | - For simple, conditional logic, use the `if` method instead. 1319 | 1320 | - Example: 1321 | 1322 | ```javascript 1323 | $("button") 1324 | .kids() 1325 | .takeWhile((el) => el.hasClass("active")) 1326 | .css("color", "red") 1327 | // will do nothing if the button does not have the active class 1328 | ``` 1329 | 1330 | ##### DomProxy.refresh 1331 | 1332 | - **refresh(): DomProxy** 1333 | 1334 | - Restores the proxy to its original state. 1335 | - Example: `$('button').css('color', 'red').next().css('color', 'blue').refresh().css('color', 'green')` 1336 | - Expectation: The button will turn green. The next sibling will remain red. 1337 | 1338 | ### DomProxyCollection 1339 | 1340 | A proxy covering a collection of HTML elements that allows you to chain methods sequentially (including asynchronous tasks) and then execute them one after the other. It includes **43** of these custom methods, but you can still use the full DOM API if you need to. 1341 | 1342 | #### DomProxyCollection Methods 1343 | 1344 | ##### DomProxyCollection.on 1345 | 1346 | - **on(ev: string, fn: EventListenerOrEventListenerObject): DomProxyCollection** 1347 | 1348 | - Adds an event listener to the elements. 1349 | - Example: `$$('button').on('click', () => console.log('clicked'))` 1350 | 1351 | ##### DomProxyCollection.once 1352 | 1353 | - **once(ev: string, fn: EventListenerOrEventListenerObject): DomProxyCollection** 1354 | 1355 | - Adds an event listener to the elements that will only fire once. 1356 | - Example: `$$('button').once('click', () => console.log('clicked'))` 1357 | 1358 | ##### DomProxyCollection.off 1359 | 1360 | - **off(ev: string, fn: EventListenerOrEventListenerObject): DomProxyCollection** 1361 | 1362 | - Removes an event listener from the elements. 1363 | - Example: `$$('button').off('click', clickHandler)` 1364 | 1365 | ##### DomProxyCollection.delegate 1366 | 1367 | - **delegate(event: string, subSelector: string, handler: EventListenerOrEventListenerObject): DomProxyCollection** 1368 | 1369 | - Delegates an event listener to the elements. 1370 | - Example: `$$('.container').delegate('click', '.buttons', (e) => console.log('Button clicked'))` 1371 | 1372 | ##### DomProxyCollection.html 1373 | 1374 | - **html(newHtml: string, outerHTML?: boolean): DomProxyCollection** 1375 | 1376 | - Change the HTML of the elements with an **UNSANITIZED** string of new HTML. This is useful if you want to add a script tag or something. If you want to sanitize the HTML, use `sanitize` instead. 1377 | 1378 | - By default, only the element's children will be replaced (innerHTML). If you want to replace the element itself (outerHTML), set the second argument to true. 1379 | 1380 | - Example: `$$('button').html('Click me!')` 1381 | - Expectation: Every button will have a span inside of it. 1382 | - Example: `$$('button').html('Click me!', true)` 1383 | - Expectation: Every button will be replaced with a span. 1384 | 1385 | ##### DomProxyCollection.sanitize 1386 | 1387 | - **sanitize: (html: string, sanitizer?: (html: string) => string) => DomProxyCollection** 1388 | 1389 | - Sanitizes a string of untusted HTML, then replaces the elements with the new, freshly sanitized HTML. This helps protect you from XSS Attacks. It uses the setHTML API under the hood, so you can provide your own sanitizer if you want with a second argument. 1390 | - Example: `$$('button').sanitize('Click me!')` 1391 | - MDN Documentation: [setHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML) 1392 | 1393 | ##### DomProxyCollection.text 1394 | 1395 | - **text(newText: string): DomProxyCollection** 1396 | 1397 | - Changes the text of the elements while retaining the tag. 1398 | - Example: `$$('.buttons').text('Click me!')` 1399 | 1400 | ##### DomProxyCollection.val 1401 | 1402 | - **val(newValue: string | number | (string | number)[] | FileList): DomProxyCollection** 1403 | 1404 | - Changes the value of all elements in the collection based on their type. For form elements such as inputs, textareas, and selects, the appropriate property (e.g., `value`, `checked`) will be adjusted. For other elements, the `textContent` property will be set. 1405 | - Example: `$$('input[type="text"]').val('New Value')` 1406 | - Example: `$$('input[type="checkbox"]').val(true)` 1407 | - Example: `$$('input[type="radio"]').val('radio1')` 1408 | - Example: `$$('input[type="file"]').val(myFileList)` 1409 | - Example: `$$('select[multiple]').val(['option1', 'option2'])` 1410 | 1411 | ##### DomProxyCollection.css 1412 | 1413 | - **css(prop: string | Record, value?: string): DomProxyCollection** 1414 | 1415 | - Adds one or more CSS Rules to the elements. If the first argument is an object, each key-value pair will be added as a CSS Rule. If the first argument is a string, it will be treated as a CSS property and the second argument will be treated as its value. 1416 | - Example: `$$('button').css('color', 'red')` 1417 | - Example: `$$('button').css({ color: 'red', backgroundColor: 'blue' })` 1418 | 1419 | ##### DomProxyCollection.addStyleSheet 1420 | 1421 | - **addStyleSheet(cssString: string): DomProxyCollection** 1422 | 1423 | - Adds a stylesheet to the ENTIRE DOCUMENT (this is useful for things like :hover styles). Got an good idea for how to make this scoped to a single element? Open a PR! 1424 | - This should be used only when you need to do something like set a pseudo-class on the fly. Otherwise, just write a real stylesheet. 1425 | - Got a good idea for how to make this scoped to a single element? Open a PR! I was thinking something like the `@scope` rule being automatically inserted, but that's still only in Chromium browsers. 1426 | - [Here's a link to the explainer for @scope](https://css.oddbird.net/scope/explainer/) 1427 | - Right now, every rule will be given an !important flag, so it will override any existing styles. This is drastic I know, but it's the only way to make this work if you're creating other inline styles. 1428 | - Example: `$$('button').addStyleSheet('button:hover { color: red }')` 1429 | 1430 | ##### DomProxyCollection.addClass 1431 | 1432 | - **addClass(className: string): DomProxyCollection** 1433 | 1434 | - Adds one or more classes to the elements. 1435 | 1436 | - To add multiple classes, separate them with commas. 1437 | 1438 | - Alternatively, you can just spread out an array. 1439 | - Example: `$$('.buttons').addClass('btn')` 1440 | - Example: `$$('.buttons').addClass('btn', 'btn-primary')` 1441 | - Example: 1442 | 1443 | ```javascript 1444 | const classes = ["btn", "btn-primary"] 1445 | $$(".buttons").addClass(...classes) 1446 | ``` 1447 | 1448 | ##### DomProxyCollection.removeClass 1449 | 1450 | - **removeClass(className: string): DomProxyCollection** 1451 | 1452 | - Removes one or more classes from the elements. 1453 | 1454 | - To remove multiple classes, separate them with commas. 1455 | 1456 | - Alternatively, you can just spread out an array. 1457 | 1458 | - Example: `$$('.buttons').removeClass('btn')` 1459 | 1460 | - Example: `$$('.buttons').removeClass('btn', 'btn-primary')` 1461 | 1462 | - Example: 1463 | 1464 | ```javascript 1465 | const classes = ["btn", "btn-primary"] 1466 | $$(".buttons").removeClass(...classes) 1467 | ``` 1468 | 1469 | ##### DomProxyCollection.toggleClass 1470 | 1471 | - **toggleClass(className: string, value?: boolean): DomProxyCollection** 1472 | 1473 | - Toggles a class on the elements. If given a second argument, it will add the class if the second argument is truthy, and remove it if the second argument is falsy. 1474 | - Example: `$$('.buttons').toggleClass('btn')` 1475 | - Example: 1476 | 1477 | ```javascript 1478 | const mediumScreen = window.matchMedia("(min-width: 768px)").matches 1479 | $$(".buttons").toggleClass("btn", mediumScreen) 1480 | ``` 1481 | 1482 | ##### DomProxyCollection.set 1483 | 1484 | - **set(attr: string | Record, value?: string): DomProxyCollection** 1485 | 1486 | - Sets an attribute on the elements. 1487 | 1488 | - If the first argument is an object, each key-value pair will be treated as an attribute name and value. 1489 | 1490 | - If the first argument is a string, it will be treated as the attribute name and the second argument will be treated as the attribute value. 1491 | 1492 | - If the value is undefined, it will be set to `""`, which is useful for boolean attributes like disabled or hidden. 1493 | 1494 | - Example: `$$('button').set('disabled')` 1495 | 1496 | - Example: `$$('button').set('disabled', 'true')` 1497 | 1498 | - Example: `$$('button').set({ disabled: 'true', hidden: 'true' })` 1499 | 1500 | ##### DomProxyCollection.unset 1501 | 1502 | - **unset(attr: string): DomProxyCollection** 1503 | 1504 | - Removes an attribute from the elements. 1505 | - Example: `$$('button').unset('disabled')` 1506 | 1507 | ##### DomProxyCollection.toggle 1508 | 1509 | - **toggle(attr: string, value?: boolean): DomProxyCollection** 1510 | 1511 | - Toggles an attribute on the elements. 1512 | 1513 | - If given a second argument, it will set the attribute if the second argument is truthy, and remove it if the second argument is falsy. 1514 | 1515 | - Example: `$$('button').toggle('disabled')` 1516 | 1517 | - Example: 1518 | 1519 | ```javascript 1520 | const mediumScreen = window.matchMedia("(min-width: 768px)").matches 1521 | $$(".buttons").toggle("disabled", mediumScreen) 1522 | ``` 1523 | 1524 | ##### DomProxyCollection.data 1525 | 1526 | - **data(attr: string | { [key: string]: string}, value?: string): DomProxyCollection** 1527 | 1528 | - Sets one or more data attributes on the elements. 1529 | 1530 | - If the first argument is an object, each key-value pair will be treated as a data attribute name and value. 1531 | 1532 | - If the first argument is a string, it will be treated as the data attribute name and the second argument will be treated as the data attribute value. 1533 | 1534 | - Example: `$$('button').data('id', '123')` 1535 | 1536 | - Example: `$$('button').data({ id: '123', name: 'myButton' })` 1537 | 1538 | ##### DomProxyCollection.attach 1539 | 1540 | - **attach(...children: (HTMLElement | DomProxy)[]): DomProxyCollection** 1541 | 1542 | - Attaches children to the elements based on the provided options. 1543 | - The children can be: 1544 | - A string of HTML 1545 | - A CSS selector 1546 | - An HTMLElement 1547 | - A DomProxy 1548 | - An array of any of the above 1549 | - The position can be: 1550 | - 'append' (default): Adds the children to the end of the elements. 1551 | - 'prepend': Adds the children to the beginning of the elements. 1552 | - 'before': Adds the children before the elements. 1553 | - 'after': Adds the children after the elements. 1554 | - The HTML is sanitized by default, which helps prevent XSS attacks. 1555 | - If you want to disable sanitization, set the `sanitize` option to `false`. 1556 | - Example: `$$('button').attach('Click me!')` 1557 | - Example: `$$('button').attach($('.container'), { position: 'prepend' })` 1558 | - Example: `$$('button').attach([$('.container'), 'Click me!'], { position: 'before' })` 1559 | - Example: `$$('button').attach('')` // No XSS attack here! 1560 | - Example: `$$('button').attach('', { sanitize: false })` // XSS attack here! 1561 | - [Handy StackOverflow Answer for Position Option](https://stackoverflow.com/questions/14846506/append-prepend-after-and-before) 1562 | 1563 | ##### DomProxyCollection.cloneTo 1564 | 1565 | - **cloneTo(parentSelector: string, options?: { position: string; all: boolean }): DomProxyCollection** 1566 | 1567 | - Clone of the elements to a new parent element in the DOM. The original elements remain in their current location. If you want to move the elements instead of cloning them, use `moveTo`. 1568 | 1569 | - The position can be: 1570 | 1571 | - 'append' (default): Adds the children to the end of the parent. 1572 | - 'prepend': Adds the children to the beginning of the parent. 1573 | - 'before': Adds the children before the parent. 1574 | - 'after': Adds the children after the parent. 1575 | 1576 | - The all option will clone the elements into each new parent in the collection. If the all option is not passed, only the first parent in the collection will be used. 1577 | 1578 | - Example: `$$('div').cloneTo('.target')` // Clones and places inside first .target element (default behavior) 1579 | - Example: `$$('div').cloneTo('.target', { position: 'after' })` // Clones and places after first .target element 1580 | - Example: `$$('div').cloneTo('.target', { all: true })` // Clones and places inside all .target elements 1581 | - Example: `$$('div').cloneTo('.target', { all: true, position: 'before' })` // Clones and places before all .target elements 1582 | - [Handy StackOverflow Answer for Position Option](https://stackoverflow.com/questions/14846506/append-prepend-after-and-before) 1583 | 1584 | ##### DomProxyCollection.moveTo 1585 | 1586 | - **moveTo(parentSelector: string, options?: { position: string }): DomProxyCollection** 1587 | 1588 | - Move the elements to a new parent element in the DOM. The original elements are moved from their current location. If you want to clone the elements instead of moving them, use `cloneTo`. 1589 | - The position can be: 1590 | - 'append' (default): Adds the children to the end of the parent. 1591 | - 'prepend': Adds the children to the beginning of the parent. 1592 | - 'before': Adds the children before the parent. 1593 | - 'after': Adds the children after the parent. 1594 | - The all option can technically be passed, but the elements will simply be attached to the last parent in the collection as there is only one element. 1595 | - Example: `$$('div').moveTo('.target')` // Moves elements inside first .target element (default behavior) 1596 | - Example: `$$('div').moveTo('.target', { position: 'before' })` // Moves elements before first .target element 1597 | - Example: `$$('div').moveTo('.target', { position: 'after' })` // Moves elements after first .target element 1598 | - [Handy StackOverflow Answer for Position Option](https://stackoverflow.com/questions/14846506/append-prepend-after-and-before) 1599 | 1600 | ##### DomProxyCollection.become 1601 | 1602 | - **become(replacement: HTMLElement | Array | DomProxy, options?: { mode?: "move" | "clone"; match?: "cycle" | "remove" }): DomProxyCollection** 1603 | 1604 | - The become method is used to replace a single element with a different element from elsewhere in the DOM. 1605 | - Under the hood, it utilizes the native `replaceWith` method but adds extra layers of functionality. 1606 | - The replacement can be a simple HTMLElement, an array of HTMLElements, or another DomProxy instance. 1607 | - Mode: 1608 | 1609 | - _clone_ (default) - This makes a copy of the replacement element to use for the DomProxy. This clone includes the element, its attributes, and all its child nodes, but does not include event listeners. The original element is left untouched. 1610 | - _move_ - This moves the replacement element to the original element's position. The original element is removed from the DOM. This is the same as calling `replaceWith` directly. 1611 | 1612 | - Example: `$$('div').become(newElement, {mode: "move"})` 1613 | - Expectation: Replaces div with newElement, literally moving it to the original div's position. 1614 | 1615 | - Example: `$$('div').become(newElement, {mode: "clone"})` 1616 | - Expectation: Replaces div with a deep clone of newElement, leaving the original newElement untouched. 1617 | 1618 | - Example: `$$('.buttons').become($$('.otherButtons'))` 1619 | - Expectation: Takes another DomProxy as the replacement. The first element of the DomProxy is chosen for the replacement. 1620 | 1621 | ##### DomProxyCollection.purge 1622 | 1623 | - **purge(): DomProxyCollection** 1624 | 1625 | - Removes the elements from the DOM. 1626 | - Example: `$$('.buttons').purge()` 1627 | 1628 | ##### DomProxyCollection.send 1629 | 1630 | - **send(options: { url?: string, json?: boolean, event?: Event, serializer?: (elements) => void } & FetchOptions) => DomProxyCollection** 1631 | 1632 | - Sends HTTP requests using each of the current elements as the body of the requests unless otherwise specified. 1633 | - This will **not** cache or dedupe the fetches-- you should probably be careful with this. 1634 | - None of the options are required-- not even the URL. 1635 | 1636 | - If you do not provide a URL the method will: 1637 | 1638 | - First, look to see if it's in a form with an action property and use that. 1639 | - If it can't find that, it will look to see if the element is a button with a formaction property and use that. 1640 | - If it can't find that, it will try to see if the element is part of a form that has an action property and use that. 1641 | - Finally, if it can't find anything else, it will use the current URL. 1642 | 1643 | - Unless the `body` option is provided, it will be created automatically based on the element type: 1644 | 1645 | - If it's a form, the entire form will be serialized using the formData API unless a custom serializer is provided. 1646 | - If it's an input, textarea, or select, the value will be used. 1647 | - If it isn't a form or one of the above elements, we will check to see if the element has a form property or one can be found with the `closest` method. If so, we will serialize the form using the formData API unless a custom serializer is provided. 1648 | - If none of the above, the element's textContent will be used. 1649 | 1650 | - If the `json` option is set to true, the request will be sent as JSON and the response will be parsed as JSON. 1651 | - Otherwise, the request will be sent as FormData and the response will be parsed as text. 1652 | 1653 | - The serializer option can be used to provide a custom function to serialize the form. It will be passed the element as an argument and should return the serialized form data. 1654 | 1655 | - This will probably break if you don't return a FormData object, but I haven't tested it. 1656 | 1657 | - Example: `$$('button').send({ url: '/api/submit' })` 1658 | - Example: `$$('button').send({ url: '/api/submit', method: 'GET' })` 1659 | - Example: `$$('button').send({ url: '/api/submit', json: true })` 1660 | 1661 | ##### DomProxyCollection.do 1662 | 1663 | - **do(fn: (el: DomProxy) => Promise): DomProxyCollection** 1664 | 1665 | - Executes an asynchronous function and waits for it to resolve before continuing the chain (can be synchronous too). 1666 | - Example: `$$('button').do(async (el) => { // The elements are passed as an argument 1667 | const response = await fetch('/api') 1668 | const data = await response.json() 1669 | el.text(data.message) // All the methods are still available 1670 | })` 1671 | 1672 | ##### DomProxyCollection.defer 1673 | 1674 | - **defer(fn: (el: DomProxy) => void): DomProxyCollection** 1675 | 1676 | - Schedules a function for deferred execution on the elements. 1677 | 1678 | - This will push the operation to the very end of the internal event loop. 1679 | 1680 | - Usually, everything will happen in sequence anyways. Given the predictability of each queue, `defer` has limited use cases and should be used sparingly. The whole point of JessQuery is to make things predictable, so you should just put the function at the end of the chain if you can. 1681 | 1682 | - This only becomes a problem if you set up an event listener using the same variable that has lots of queued behavior-- especially calls to the wait method. Just wrap the wait call and everything after it in defer to ensure that event handlers don't get stuck behind these in the queue. 1683 | 1684 | - `defer` will capture the elements at the time of the call, so this should not be mixed with context switching methods like `parent` or `pickAll`. 1685 | 1686 | - Honestly, I'm not sure if this even makes much sense. I just spent a bunch of time building a crazy queue system, and I feel like I need to expose it. If you have any ideas for how to make this more useful, please open an issue or PR. 1687 | 1688 | - Example: 1689 | 1690 | ```javascript 1691 | const buttons = $$(".buttons") 1692 | 1693 | buttons 1694 | .text("this won't do anything for a second because of the wait call") 1695 | .on("click", () => buttons.text("clicked")) 1696 | .wait(1000) 1697 | 1698 | //but if we wrap the wait call in defer, the events will not be queued behind it 1699 | buttons 1700 | .text("this will be immediately responsive due to the defer call") 1701 | .defer((el) => el.wait(1000).text("Yay!")) 1702 | 1703 | // THIS ONLY OCCURS BECAUSE THE SAME VARIABLE IS USED FOR THE EVENT LISTENER AND THE CHAIN 1704 | $$(".buttons").on("click", () => 1705 | $$(".buttons").text("clicked").wait(1000) 1706 | ) // NO PROBLEM HERE 1707 | `` 1708 | ``` 1709 | 1710 | ##### DomProxyCollection.fromJSON 1711 | 1712 | - **fromJSON(url: string, transformFunc: (el: DomProxy, json: any) => void, options?: FetchOptions): DomProxyCollection** 1713 | 1714 | - Fetches a JSON resource from the provided URL and applies a transformation function which uses the fetched JSON and the proxy's target element as arguments. 1715 | - The transform function can be used to set the text, html, or any other property of the element. 1716 | - The options object can be used define a plethora of options defined in [FetchOptions](#FetchOptions). 1717 | 1718 | - These options extend the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) interface, so you can use all of the normal fetch options as well. (e.g., method, headers, etc.) 1719 | 1720 | - Example: 1721 | 1722 | ```javascript 1723 | $$(".item").fromJSON("/api/data", (element, json) => { 1724 | element.text(json.value) 1725 | }) 1726 | ``` 1727 | 1728 | - Example: 1729 | 1730 | ```javascript 1731 | $$(".item").fromJSON("/api/data", (element, json) => { 1732 | element.html(`${json.description}`) 1733 | }) 1734 | ``` 1735 | 1736 | - Example: 1737 | 1738 | ```javascript 1739 | $$('.news-item').fromJSON('/api/news-item', (element, json) => { 1740 | { title, summary } = json; 1741 | 1742 | element.html(`

${title}

1743 |

${summary}

`); 1744 | }, 1745 | { 1746 | error: 'Failed to load news item', 1747 | onWait: () => `

Loading news item...

`, 1748 | onSuccess: () => console.log('News item loaded') 1749 | } 1750 | ``` 1751 | 1752 | ##### DomProxyCollection.fromHTML 1753 | 1754 | - **fromHTML(url: string, options?: FetchOptions): DomProxyCollection** 1755 | 1756 | - Fetches an HTML resource from the provided URL and inserts it into the proxy's target element. 1757 | - The options object can be used define a plethora of options defined in [FetchOptions](#FetchOptions). 1758 | - These options extend the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) interface, so you can use all of the normal fetch options as well. (e.g., method, headers, etc.) 1759 | - The HTML is sanitized by default, which helps prevent XSS attacks. 1760 | - Example: `$$('.template').fromHTML('/template.html')` 1761 | - Example: `$$('.update').fromHTML('/update.html', { fallback: 'Loading update...', error: 'Failed to load update!' })` 1762 | - Example: `$$('.content').fromHTML('/malicious-content.html', { sanitize: false })` 1763 | 1764 | ##### DomProxyCollection.fromStream 1765 | 1766 | - **fromStream(url: string, options?: { sse?: boolean; add?: boolean; toTop?: boolean; error?: string; onError?: () => void; onWait?: () => void; waitTime?: number; runScripts?: boolean; sanitize?: boolean; onSuccess?: (data: any) => void }): DomProxyCollection** 1767 | 1768 | - Dynamically fetches data from the provided URL and updates a single DOM element using a stream or Server-Sent Event (SSE). 1769 | - This includes all the same options as the `fromHTML` and `fromJSON` methods for normal streams, but with a few caveats for SSEs. 1770 | - the `onSuccess` callback will be invoked after each message is received, and it will receive the last event as an argument. 1771 | - SSE's have two additional options, `add` and `onTop`, which determine how the new data is added to the existing content. 1772 | - The options object can be used define a plethora of options defined in [FetchOptions](#FetchOptions). 1773 | - These options extend the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) interface, so you can use all of the normal fetch options as well. (e.g., method, headers, etc.) 1774 | - The HTML is sanitized by default, which helps prevent XSS attacks. 1775 | - Example: `$$('.content').fromStream('/api/data', { sanitize: false })` <-- Only for trusted sources! 1776 | - Example: `$$('.liveFeed').fromStream('/api/live', { sse: true, add: true, onSuccess: (data) => console.log('New data received:', data) })` 1777 | 1778 | ##### DomProxyCollection.transition 1779 | 1780 | - **transition(keyframes: Keyframe[] | PropertyIndexedKeyframes, options: KeyframeAnimationOptions): DomProxyCollection** 1781 | 1782 | - Animates the elements using the WAAPI. 1783 | - Returns the proxy so you can continue chaining. If you need to return the animation object, use the `animate` method instead. 1784 | - Remember, this method is blocking, so watch out for any event handlers using the same variable. 1785 | - Example: `$$('.buttons').transition([{ opacity: 0 }, { opacity: 1 }], { duration: 1000 })` 1786 | - [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate) 1787 | 1788 | ##### DomProxyCollection.wait 1789 | 1790 | - **wait(ms: number): DomProxyCollection** 1791 | 1792 | - Waits for a specified number of milliseconds before continuing the chain. 1793 | - Returns the proxy so you can continue chaining. If you need to return the animation object, use the `animate` method instead. 1794 | - Remember, this method is blocking, so watch out for any event handlers using the same variable. 1795 | - If you do not want to switch to a new variable, make sure that you use this in combination with `defer` which will allow the events to skip ahead in the queue. 1796 | - `wait` will **always** reliably hold up a queue when used in the middle of a chain, but each method that contains the element as an argument will operate on a different queue so `wait()` calls inside of them will not be respected in subsequent methods on the parent chain. 1797 | - These methods, such as `do`, `fromJSON`, and `if`, will respect `wait` calls held within them for their own individual queues, but they will not hold up the queue of the parent chain. 1798 | - So, if you call `wait` inside the callback for the method, you must continue the chain inside. Otherwise, the chain will continue immediately. 1799 | - Example: `$$('buttons').wait(1000)` 1800 | - Example: 1801 | 1802 | ```javascript 1803 | testButtons 1804 | .on("click", () => { 1805 | testButtons.text(`Clicked ${++count} times`) 1806 | }) 1807 | .wait(2000) 1808 | .css({ color: "red" }) // BAD! The click handler will not register for 2 seconds! 1809 | 1810 | testButtons 1811 | .on("click", () => { 1812 | testButtons.text(`Clicked ${++count} times`) 1813 | }) 1814 | .defer((el) => el.wait(2000).css({ color: "red" })) // Good! The click handler will register immediately! 1815 | 1816 | testButtons 1817 | .on("click", () => { 1818 | $(".testButton").text(`Clicked ${++count} times`) 1819 | }) 1820 | .wait(2000) 1821 | .css({ color: "red" }) // Less good, but still better! 1822 | // You made a new proxy for no reason, but at least the user can click the button! 1823 | 1824 | testButtons 1825 | .do((el) => el.wait(2000).css({ color: "green" }).wait(2000)) 1826 | .css({ color: "red" }) 1827 | // This will turn red, then green. NOT GREEN, THEN RED! 1828 | ``` 1829 | 1830 | ##### DomProxyCollection.next 1831 | 1832 | - **next(): DomProxyCollection** 1833 | 1834 | - Switch to the next siblings of the elements in the middle of a chain. 1835 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1836 | - Example: `$$('button').css('color', 'red').next().css('color', 'blue')` 1837 | - Expectation: The next siblings of the buttons will turn blue. The buttons themselves will remain red. 1838 | 1839 | ##### DomProxyCollection.prev 1840 | 1841 | - **prev(): DomProxyCollection** 1842 | 1843 | - Switch to the previous siblings of the elements in the middle of a chain. 1844 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1845 | - Example: `$$('button').css('color', 'red').prev().css('color', 'blue')` 1846 | - Expectation: The previous siblings of the buttons will turn blue. The buttons themselves will remain red. 1847 | 1848 | ##### DomProxyCollection.first 1849 | 1850 | - **first(): DomProxyCollection** 1851 | 1852 | - Switch to the first children of the elements in the middle of a chain. 1853 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1854 | - Example: `$$('button').css('color', 'red').first().css('color', 'blue')` 1855 | - Expectation: The first children of the buttons will turn blue. The buttons themselves will remain red. 1856 | 1857 | ##### DomProxyCollection.last 1858 | 1859 | - **last(): DomProxyCollection** 1860 | 1861 | - Switch to the last children of the elements in the middle of a chain. 1862 | - Example: `$$('button').css('color', 'red').last().css('color', 'blue')` 1863 | - Expectation: The last children of the buttons will turn blue. The buttons themselves will remain red. 1864 | 1865 | ##### DomProxyCollection.parent 1866 | 1867 | - **parent(): DomProxyCollection** 1868 | 1869 | - Switch to the parents of the elements in the middle of a chain. 1870 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1871 | - Example: `$$('button').css('color', 'red').parent().css('color', 'blue')` 1872 | - Expectation: The parents of the buttons will turn blue. The buttons themselves will remain red. 1873 | 1874 | ##### DomProxyCollection.ancestor 1875 | 1876 | - **ancestor(ancestorSelector: string): DomProxyCollection** 1877 | 1878 | - Switch to the closest ancestors matching a selector. Uses the `closest` API under the hood. 1879 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1880 | - Example: `$$('.buttons').css('color', 'red').ancestor('.container').css('color', 'blue')` 1881 | - Expectation: The containers will turn blue. The buttons will remain red. 1882 | 1883 | ##### DomProxyCollection.pick 1884 | 1885 | - **pick(subSelector: string): DomProxyCollection** 1886 | 1887 | - Switch to the first descendants matching a sub-selector. 1888 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1889 | - Example: `$$('.container').css('color', 'red').pick('.buttons').css('color', 'blue')` 1890 | - Expectation: The buttons will turn blue. The container will remain red. 1891 | 1892 | ##### DomProxyCollection.pickAll 1893 | 1894 | - **pickAll(subSelector: string): DomProxyCollection** 1895 | 1896 | - Switch to all descendants matching a sub-selector. 1897 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1898 | - Example: `$$('.container').css('color', 'red').pickAll('.buttons').css('color', 'blue')` 1899 | - Expectation: The buttons will turn blue. The container will remain red. 1900 | 1901 | ##### DomProxyCollection.siblings 1902 | 1903 | - **siblings(): DomProxyCollection** 1904 | 1905 | - Switch to the siblings of the elements in the middle of a chain. 1906 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1907 | - Example: `$$('button').css('color', 'red').siblings().css('color', 'blue')` 1908 | - Expectation: The siblings of the buttons will turn blue. The buttons themselves will remain red. 1909 | 1910 | ##### DomProxyCollection.kids 1911 | 1912 | - **kids(): DomProxyCollection** 1913 | 1914 | - Switch to the children of the elements in the middle of a chain. 1915 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1916 | - Example: `$$('.container').css('color', 'red').kids().css('color', 'blue')` 1917 | - Expectation: The children of the container will turn blue. The container itself will remain red. 1918 | 1919 | ##### DomProxyCollection.if 1920 | 1921 | - **if: (options: { is: (el: DomProxyCollection) => boolean, then?: (el: DomProxyCollection) => void, or?: (el: DomProxyCollection) => void }) => DomProxyCollection** 1922 | 1923 | - Executes conditional logic on the elements based on their current state. 1924 | - The `is` option is a function that receives the elements as an argument and returns a boolean. 1925 | - The `then` option is a function that receives the elements as an argument and is executed if the `is` function returns true. 1926 | - The `or` option is a function that receives the elements as an argument and is executed if the `is` function returns false. 1927 | 1928 | - Example: 1929 | 1930 | ```javascript 1931 | $$(".buttons").if({ 1932 | is: (el) => el.hasClass("active"), 1933 | then: (el) => el.text("Active"), 1934 | or: (el) => el.text("Inactive"), 1935 | }) 1936 | ``` 1937 | 1938 | ##### DomProxyCollection.takeWhile 1939 | 1940 | - **takeWhile: (predicate: (el: DomProxyCollection, reverse: boolean) => boolean) => DomProxyCollection** 1941 | 1942 | - Filters the current elements in the proxy based on a predicate. 1943 | - The predicate is a function that receives the elements as an argument and returns a boolean. 1944 | - The predicate will be executed on each element in the proxy in order. If the predicate returns true, the element will be kept. If the predicate returns false, the element will be removed. 1945 | - It will run on the elements in the order they were added to the proxy unless the `reverse` argument is set to true. In that case, it will run in reverse order. Usually, this will correspond to position in the DOM, but it's not guaranteed. 1946 | - It's essential to use this method with caution as it can empty the proxy if the current elements do not match the predicate. 1947 | - The `refresh()` method can be used to restore the proxy to its original state. 1948 | - This will throw an error if the proxy was created as "fixed" (with a second argument of true). 1949 | - For simple, conditional logic, use the `if` method instead. 1950 | 1951 | - Example: 1952 | 1953 | ```javascript 1954 | $$(".buttons") 1955 | .kids() 1956 | .takeWhile((el) => el.hasClass("active")) 1957 | .css("color", "red") 1958 | // will do nothing if the buttons do not have the active class 1959 | ``` 1960 | 1961 | ##### DomProxyCollection.refresh 1962 | 1963 | - **refresh(): DomProxyCollection** 1964 | 1965 | - Restores the proxy to its original state. 1966 | - Example: `$$('button').next().css('color', 'blue').refresh().css('color', 'green')` 1967 | - Expectation: Every button will turn green. Each of their next siblings will remain blue. 1968 | 1969 | ### FetchOptions 1970 | 1971 | - **onError?: () => void** 1972 | 1973 | - A callback to execute if the fetch fails. 1974 | - There is a standard, boring error message that will replace the element if no `onError` callback is provided. 1975 | - This will either simply contain the error message or `Failed to load ${type}` (e.g., `Failed to load HTML`) 1976 | 1977 | - **onSuccess?: () => void** 1978 | 1979 | - A callback to execute if the fetch succeeds. 1980 | - If you are using `fromStream()` and the `sse` option is set to true, this will receive the last event as an argument. 1981 | 1982 | - **onWait?: () => void** 1983 | 1984 | - A callback to execute while the fetch is pending. 1985 | 1986 | - This will not run for 250ms by default, but you can change that by setting the `waitTime` option. 1987 | 1988 | - The error message to display if the fetch fails. 1989 | - If you provide an onError callback, the error message will be ignored. 1990 | 1991 | - **waitTime?: number** 1992 | 1993 | - The number of milliseconds to wait before executing the `onWait` callback. 1994 | - This defaults to 250ms. 1995 | 1996 | - **retries?: number** 1997 | 1998 | - The number of times to retry the fetch if it fails. 1999 | - This defaults to 0. 2000 | 2001 | - **retryDelay?: number** 2002 | 2003 | - The number of milliseconds to wait before retrying the fetch. 2004 | - This defaults to 1000ms. 2005 | 2006 | - **runScripts?: boolean** 2007 | 2008 | - Whether or not to run scripts in the fetched HTML. 2009 | - This defaults to false. 2010 | 2011 | - **sanitize?: boolean** 2012 | 2013 | - Whether or not to sanitize the fetched HTML. 2014 | - This defaults to true. 2015 | 2016 | - **Sanitizer?:** 2017 | 2018 | - The sanitizer to use for the fetched HTML. 2019 | - This defaults to the default sanitizer. 2020 | - [Here's the MDN docs for the Sanitizer API](https://developer.mozilla.org/en-US/docs/Web/API/Sanitizer/Sanitizer) 2021 | 2022 | - **sse?: boolean** 2023 | 2024 | - **Only in `fromStream()`** 2025 | - Whether or not to use Server-Sent Events (SSE) instead of a normal stream. 2026 | - This defaults to false. 2027 | 2028 | - **add?: boolean** 2029 | 2030 | - **Only for SSE's** 2031 | - Whether or not to add the new data to the existing content. 2032 | - This defaults to false. 2033 | 2034 | - **toTop?: boolean** 2035 | 2036 | - **Only for SSE's** 2037 | - Whether or not to add the new data to the top of the existing content. 2038 | - This defaults to false. 2039 | 2040 | - Finally, all of the normal [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) options are available as well. 2041 | 2042 | ## Contributing 2043 | 2044 | If you have any ideas for new features or improvements, feel free to open an issue or a PR. I'm always open to suggestions! I started this as a bit of a joke, but I think it turned into something pretty useful. I'm sure there are a lot of things that could be improved, so I welcome any and all feedback. 2045 | -------------------------------------------------------------------------------- /ajax.js: -------------------------------------------------------------------------------- 1 | export async function wrappedFetch(url, options, type, target) { 2 | const { onWait, waitTime, onSuccess, onError, retryDelay = 1000 } = options 3 | let waitTimeout = null 4 | onWait && (waitTimeout = setTimeout(() => onWait(), waitTime || 250)) 5 | 6 | try { 7 | const response = await fetch(url, options) 8 | const data = await handleResponse(response, type, target, options) 9 | onSuccess && requestIdleCallback(() => onSuccess(data)) 10 | return data 11 | } catch (error) { 12 | if (options.retries > 0) { 13 | const newOptions = { 14 | ...options, 15 | retries: options.retries - 1, 16 | retryDelay: options.retryDelay * 2, 17 | } 18 | await new Promise((resolve) => setTimeout(resolve, retryDelay)) 19 | return wrappedFetch(url, newOptions, type, target) 20 | } 21 | const errorMessage = error.message || error || `Failed to load ${type}` 22 | onError 23 | ? requestIdleCallback(() => onError(error)) 24 | : target.forEach((el) => (el.innerHTML = errorMessage)) 25 | } finally { 26 | clearTimeout(waitTimeout) 27 | } 28 | } 29 | 30 | export function send(element, options = {}, target) { 31 | let { url, method = "POST", json = false, body, event, headers } = options 32 | 33 | event && event.preventDefault() 34 | url = url || getAction(element) 35 | headers = headers ? new Headers(headers) : new Headers() 36 | body = body || getBody(element, options) 37 | if (method === "GET" || method === "HEAD") { 38 | body = null 39 | } 40 | 41 | if (json) { 42 | headers.append("Content-Type", "application/json") 43 | body = 44 | body instanceof FormData 45 | ? JSON.stringify(Object.fromEntries(body)) 46 | : typeof body === "object" 47 | ? JSON.stringify(body) 48 | : JSON.stringify({ body }) 49 | } 50 | 51 | const fetchOptions = { 52 | ...options, 53 | method, 54 | headers, 55 | body, 56 | } 57 | 58 | return wrappedFetch(url, fetchOptions, "text", target) 59 | } 60 | 61 | export function fetchElements(type, url, options = {}, target) { 62 | if (type === "sse") { 63 | const eventSource = new EventSource(url) 64 | eventSource.onmessage = (event) => { 65 | target.forEach((el) => { 66 | if (options.add) { 67 | options.toTop 68 | ? sanitizeOrNot(el, event.data + "
" + el.innerHTML, options) 69 | : sanitizeOrNot(el, el.innerHTML + "
" + event.data, options) 70 | } else { 71 | sanitizeOrNot(el, event.data, options) 72 | } 73 | }) 74 | options.onSuccess && requestIdleCallback(() => options.onSuccess(event)) 75 | options.runScripts && runScripts(target) 76 | } 77 | eventSource.onerror = (error) => options.onError && options.onError(error) 78 | return 79 | } 80 | 81 | wrappedFetch(url, options, type, target).then((data) => { 82 | if (!data) throw new Error(`No data received from ${url}`) 83 | if (type === "text") { 84 | sanitizeOrNot(target, data, options) 85 | } 86 | options.runScripts && runScripts(target) 87 | }) 88 | } 89 | 90 | function handleResponse(response, type, target, options) { 91 | if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`) 92 | 93 | if (type === "json") return response.json() 94 | if (type === "stream") { 95 | const reader = response.body.getReader() 96 | return recursiveRead(reader, target, type, options) 97 | } 98 | 99 | return response.text() 100 | } 101 | 102 | function recursiveRead(reader, target, type, options, chunks = []) { 103 | return reader.read().then(({ done, value }) => { 104 | const decodedChunk = new TextDecoder("utf-8").decode(value) 105 | const allChunks = [...chunks, decodedChunk] 106 | 107 | target.forEach((el) => { 108 | sanitizeOrNot(el, allChunks.join(""), options) 109 | }) 110 | 111 | return done 112 | ? allChunks.join("") 113 | : recursiveRead(reader, target, type, options, allChunks) 114 | }) 115 | } 116 | 117 | function getBody(element, options = {}) { 118 | const { serializer } = options 119 | const tagName = element.tagName 120 | const form = element.form || element.closest("form") 121 | 122 | return tagName === "FORM" // If the element is a form 123 | ? serializer // & a serializer is passed 124 | ? serializer(element) // use the serializer 125 | : new FormData(element) // otherwise use FormData on the form 126 | : // If the element is an input, select, or textarea 127 | tagName === "INPUT" || tagName === "SELECT" || tagName === "TEXTAREA" 128 | ? element.value // use the value 129 | : form // If the element is not a form, but has a form ancestor 130 | ? serializer // & a serializer is passed 131 | ? serializer(form) // use the serializer 132 | : new FormData(form) // otherwise use FormData on the ancestor form 133 | : element.textContent // If nothing else, just use the text content 134 | } 135 | 136 | function getAction(element) { 137 | let form = element.form || element.closest("form") 138 | return element.formAction && element.formAction !== window.location.href 139 | ? element.formAction // If there is a formaction, but it is not the same as the current URL, use it 140 | : element.action // If there is an action attribute 141 | ? element.action // use it 142 | : form && form.action // If there is no action, but there is a form ancestor with an action 143 | ? form.action // use it 144 | : window.location.href // If there is no formAction, no action, and no form ancestor with an action, use the current URL 145 | } 146 | 147 | function sanitizeOrNot(target, data, options) { 148 | const { sanitize = true, sanitizer } = options 149 | const targetElements = Array.isArray(target) ? target : [target] 150 | targetElements.forEach((el) => { 151 | sanitize ? el.setHTML(data, { sanitizer }) : (el.innerHTML = data) 152 | }) 153 | } 154 | 155 | function runScripts(target) { 156 | target.forEach((el) => 157 | el.querySelectorAll("script").forEach((script) => { 158 | const newScript = document.createElement("script") 159 | newScript.textContent = script.textContent 160 | newScript.type = script.type 161 | script.replaceWith(newScript) 162 | }) 163 | ) 164 | } 165 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | import { errorHandler, giveContext } from "./errors.js" 2 | import { addMethods } from "./methods.js" 3 | import { getDOMElement } from "./DOM.js" 4 | 5 | export function $(selector, fixed = false) { 6 | return createProxy("$", selector, fixed) 7 | } 8 | 9 | export function $$(selector, fixed = false) { 10 | return createProxy("$$", selector, fixed) 11 | } 12 | 13 | function createProxy(type, selector, fixed = false) { 14 | const elements = getDOMElement(selector, { 15 | all: type === "$$", 16 | sanitize: false, 17 | }) 18 | 19 | if (!elements[0]) { 20 | return errorHandler(new Error(`No elements.`), giveContext(type, selector)) 21 | } 22 | 23 | return addMethods(selector, elements, fixed) 24 | } 25 | 26 | export function createQueue() { 27 | const mainQueue = [] 28 | const deferredQueue = [] 29 | let isRunning = false 30 | 31 | async function runQueue() { 32 | if (isRunning) return 33 | isRunning = true 34 | 35 | while (mainQueue.length > 0) { 36 | const fn = mainQueue.shift() 37 | await fn() 38 | } 39 | 40 | if (deferredQueue.length > 0 && mainQueue.length === 0) { 41 | while (deferredQueue.length > 0) { 42 | const { fn, args } = deferredQueue.shift() 43 | await eachArgument(fn, args) 44 | } 45 | } 46 | 47 | isRunning = false 48 | } 49 | 50 | function addToQueue(fn) { 51 | mainQueue.push(fn) 52 | runQueue() 53 | } 54 | 55 | function defer(fn, args = []) { 56 | deferredQueue.push({ fn, args }) 57 | if (!isRunning) { 58 | runQueue() 59 | } 60 | } 61 | 62 | return { 63 | addToQueue, 64 | defer, 65 | } 66 | } 67 | 68 | export function queueAndReturn(addToQueue, getProxy) { 69 | return function queueFunction(fn, context, eager = true) { 70 | return (...args) => { 71 | addToQueue(async () => { 72 | try { 73 | await eachArgument(fn, args, eager) 74 | } catch (error) { 75 | errorHandler(error, context) 76 | } 77 | }) 78 | return getProxy() 79 | } 80 | } 81 | } 82 | 83 | export function handlerMaker(elements, customMethods) { 84 | return { 85 | get(_, prop) { 86 | if (elements.length && elements.length === 1) { 87 | elements = elements[0] 88 | } 89 | 90 | if (prop === "raw") { 91 | return elements 92 | } 93 | 94 | if (prop in customMethods) { 95 | return customMethods[prop] 96 | } 97 | 98 | if (Array.isArray(elements) && typeof elements[prop] === "function") { 99 | return function (...args) { 100 | const result = elements[prop](...args) 101 | return result instanceof Array 102 | ? addMethods(elements[prop].name + args, result) 103 | : addMethods(elements[prop].name + args, [result]) 104 | } 105 | } 106 | 107 | return typeof elements[prop] === "function" 108 | ? elements[prop].bind(elements) 109 | : elements[prop] 110 | }, 111 | } 112 | } 113 | 114 | async function eachArgument(fn, args, eager = true) { 115 | const isThenable = (value) => value && typeof value.then === "function" 116 | const resolvedArgs = [] 117 | for (const arg of args) { 118 | resolvedArgs.push(isThenable(arg) && eager ? await arg : arg) 119 | } 120 | const result = fn(...resolvedArgs) 121 | if (isThenable(result)) { 122 | await result 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | export let errorHandler = (error, context) => { 2 | console.error(context, error) 3 | } 4 | 5 | export function setErrorHandler(handler) { 6 | errorHandler = handler 7 | } 8 | 9 | export function giveContext(methodName, selector) { 10 | const message = 11 | methodName === "$" 12 | ? `$('${selector}')` 13 | : methodName === "$$" 14 | ? `$$('${selector}')` 15 | : `Method: ${methodName} called on: ${selector}` 16 | 17 | return message 18 | } 19 | -------------------------------------------------------------------------------- /examples/arrayMethods.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Benchmark Test 7 | 8 | 9 |
10 | 11 | 19 |
20 | 21 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /examples/attach.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Attach Test 7 | 8 | 9 |
hi
10 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/attach2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Attach Test for HTMLElement 7 | 8 | 9 |
10 | 11 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/basicUsage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 |
In one second!
12 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/become.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Become Test 7 | 8 | 9 |
10 |
To Be Replaced
11 |
Replacement Element
12 | 13 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/become2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Become Multi-Element Test 7 | 8 | 9 |
10 |
To Be Replaced 1
11 |
To Be Replaced 2
12 |
Replacement Element 1
13 |
Replacement Element 2
14 | 15 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/become3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Become Method Test 7 | 8 | 9 |
10 | 11 |
12 |

Cycle Match Test:

13 |
To Be Replaced 1
14 |
To Be Replaced 2
15 |
To Be Replaced 3
16 |
Replacement Element 1
17 |
Replacement Element 2
18 |
19 | 20 |
21 |

Remove Match Test:

22 |
To Be Replaced 1
23 |
Replacement Element 1
24 |
Replacement Element 2
25 |
26 | 27 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /examples/benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Benchmark Test 7 | 8 | 9 |
10 | 11 | 19 |
20 | 21 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /examples/cloneTo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CloneTo Test 7 | 8 | 9 |
10 |
Original Element
11 |
Two
12 |
Two
13 |
Parent
14 | 15 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/contextSwitching.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 |

jQuery is for old people.

11 |
12 | 13 | 14 | 15 | 16 |
17 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/createElement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
hi
10 | 11 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/defer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Do Method Test 7 | 8 | 9 |
10 | 11 | 12 | 13 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/defer2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Defer Method Test 7 | 8 | 9 |
10 | 11 | 12 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/do.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 |
hi
11 |
yo
12 |
sup
13 |
14 |
15 | 16 | 17 | 18 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/doublecash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | $$ Test 7 | 30 | 31 | 32 |
Div 1
33 |
Div 2
34 |
Div 3
35 |
Div 4
36 |
Scale Div
37 |
Rotate Div
38 |
Other Div
39 |
Element
40 |
Element
41 | 42 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /examples/errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error Handling Test 7 | 8 | 9 |
10 | 11 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/fromHTML.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Next Method 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/fromJSON.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test fromJSON 7 | 8 | 9 |
10 |
11 | 12 | 40 | 41 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/fromStream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Next Method 7 | 8 | 9 |
I'll still be here.
10 |
I'll be replaced with a stream soon.
11 |
I'm sticking around
12 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/if.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 12 | 13 | 14 |
0
15 | 16 | 17 | 18 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/if2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TakeWhile Demo 7 | 12 | 13 | 14 | 1 15 | 2 16 | 3 17 | 4 18 | 5 19 | 6 20 | 7 21 | 22 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/moveOrClone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test cloneTo and moveTo 7 | 21 | 22 | 23 |
24 |

Container 1

25 |
Demo Element A
26 |
Demo Element B
27 |
28 | 29 |
30 |

Container 2

31 |
32 | 33 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/moveTo2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MoveTo Multi-Element Test 7 | 8 | 9 |
10 |
Original Element 1
11 |
Original Element 2
12 |
New Parent
13 |
New Parent
14 | 15 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/moveTo3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MoveTo Before Test 7 | 8 | 9 |
Original Element
10 |
11 |
Child
12 |
13 | 14 |
15 | 16 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/oldBadTests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Function Tests 7 | 8 | 13 | 14 | 15 | 16 | 17 |
18 | Delegate Test: Click me 19 | NO, click me.... Like, a lot! 20 | 21 |
22 |
Test Div 1
23 |
Test Div 2
24 | 25 | 29 |
30 |
31 | 32 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /examples/parentKids.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test parent function 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/perilsFixedOrNot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /examples/perilsOfWait.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 |
hi
11 |
yo
12 |
sup
13 |
14 |
15 | 16 | 17 | 18 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /examples/promisify - audio API.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web Audio API 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/promisify - canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Canvas Animation 7 | 8 | 9 | 15 | 16 | 17 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/promisify - fileReader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | File Reader 7 | 8 | 9 | 10 |
File contents will appear here...
11 | 12 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/promisify - geolocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
hi
10 | 11 | 12 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/promisify - indexedDB.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IndexedDB 7 | 8 | 9 |
Click to save data to IndexedDB...
10 | 11 | 12 | 13 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /examples/promisify - rAF.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Animation 7 | 23 | 24 | 25 |
26 |
27 |
28 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/promisify - setInterval.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Promisify XHR Example 7 | 13 | 14 | 15 | 16 |
17 | 18 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /examples/promisify - xhr elaborate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Promisify XHR Example 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /examples/promisify - xhr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Promisify XHR Example 7 | 13 | 14 | 15 | 16 |
17 | 18 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/runScripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/sanitize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DOM Manipulation with $ and $$ 7 | 8 | 9 |
10 |

This is a paragraph.

11 |

This is another paragraph.

12 |
13 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/send.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test send Function 7 | 8 | 9 |
10 | 16 | 17 | 18 |
19 | 20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/send2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test send Function 7 | 8 | 9 |
10 | I AIN'T DOIN' NOTHIN' 11 |
12 |
13 | 14 | 15 | 16 |
17 | 18 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/send3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Wrapped Fetch Test 7 | 8 | 9 |
Data will appear here...
10 | 11 | 12 | 13 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | import http from "http" 2 | import url from "url" 3 | 4 | // // ** Submissions ** 5 | 6 | // const server = http.createServer((req, res) => { 7 | // console.log(req.method, req.url) 8 | 9 | // // Set CORS headers 10 | // res.setHeader("Access-Control-Allow-Origin", "http://localhost:5500") 11 | // res.setHeader("Access-Control-Allow-Methods", "POST") 12 | // res.setHeader("Access-Control-Allow-Headers", "Content-Type") 13 | 14 | // if (req.method === "OPTIONS") { 15 | // res.statusCode = 200 16 | // res.end() 17 | // return 18 | // } 19 | 20 | // if (req.method === "POST") { 21 | // const queryParams = new url.URL(req.url, `http://${req.headers.host}`) 22 | // .searchParams 23 | 24 | // const delay = parseInt(queryParams.get("delay")) || 0 25 | 26 | // let requestData = "" 27 | 28 | // req.on("data", (chunk) => { 29 | // requestData += chunk 30 | // }) 31 | 32 | // req.on("end", () => { 33 | // setTimeout(() => { 34 | // // Use setTimeout here 35 | // console.log("Received data:") 36 | // console.log(requestData) 37 | // res.statusCode = 200 38 | // res.end( 39 | // requestData + 40 | // "\n" + 41 | // "JESSE IS THE BEST" + 42 | // "\n" + 43 | // new Date().toLocaleTimeString() 44 | // ) 45 | // }, delay) // Delay the response by the specified amount 46 | // }) 47 | // } else { 48 | // res.statusCode = 404 49 | // res.end("Not Found") 50 | // } 51 | // }) 52 | 53 | // server.listen(3000, () => { 54 | // console.log(`click here: http://localhost:3000`) 55 | // }) 56 | 57 | // ** STREAMING ** 58 | 59 | const server = http.createServer((req, res) => { 60 | // Allow requests from any origin 61 | res.setHeader("Access-Control-Allow-Origin", "*") 62 | 63 | // Set other CORS headers as needed 64 | res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") 65 | res.setHeader("Access-Control-Allow-Headers", "Content-Type") 66 | 67 | // Respond with the data 68 | res.writeHead(200, { "Content-Type": "application/octet-stream" }) 69 | 70 | res.write("

hello

") 71 | 72 | // Simulate streaming by sending chunks of data every 1 second 73 | const intervalId = setInterval(() => { 74 | res.write(`

${new Date().toLocaleTimeString()} \n

`) 75 | res.write(``) 76 | }, 1000) 77 | 78 | // End the stream and clear the interval after 5 seconds 79 | setTimeout(() => { 80 | clearInterval(intervalId) 81 | res.end() 82 | }, 5000) 83 | 84 | // Clear the interval if the client disconnects before 5 seconds 85 | res.on("close", () => clearInterval(intervalId)) 86 | }) 87 | 88 | server.listen(8080, () => { 89 | console.log("Server is running on port 8080") 90 | }) 91 | 92 | // ** SERVER SENT EVENTS ** 93 | 94 | // const server = http.createServer((req, res) => { 95 | // // Allow requests from any origin 96 | // res.setHeader("Access-Control-Allow-Origin", "*") 97 | 98 | // // Set other CORS headers as needed 99 | // res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") 100 | // res.setHeader("Access-Control-Allow-Headers", "Content-Type") 101 | 102 | // // Check if the client requested SSE 103 | // if (req.headers.accept && req.headers.accept === "text/event-stream") { 104 | // // Set headers for SSE 105 | // res.writeHead(200, { 106 | // "Content-Type": "text/event-stream", 107 | // "Cache-Control": "no-cache", 108 | // "Connection": "keep-alive", 109 | // }) 110 | 111 | // // Send an event every second 112 | // const intervalId = setInterval(() => { 113 | // res.write( 114 | // `data: The server time is: ${new Date().toLocaleTimeString()}\n\n` 115 | // ) 116 | // }, 1000) 117 | 118 | // // Close the connection when the client disconnects 119 | // req.on("close", () => { 120 | // clearInterval(intervalId) 121 | // res.end() 122 | // }) 123 | // } else { 124 | // // Handle other requests (e.g., API requests, static files, etc.) 125 | // res.writeHead(404) 126 | // res.end() 127 | // } 128 | // }) 129 | 130 | // server.listen(8080, () => { 131 | // console.log("Server is running on port 8080") 132 | // }) 133 | 134 | // // **RETRIES** 135 | // let requestCounter = 0 136 | // let firstRequestTime = null 137 | 138 | // const server = http.createServer((req, res) => { 139 | // const parsedUrl = new url.URL(req.url, `http://${req.headers.host}`) 140 | 141 | // console.log(req.method, req.url) 142 | 143 | // // Set CORS headers 144 | // res.setHeader("Access-Control-Allow-Origin", "*") 145 | // res.setHeader("Access-Control-Allow-Methods", "GET") 146 | // res.setHeader("Access-Control-Allow-Headers", "Content-Type") 147 | 148 | // if (req.method === "OPTIONS") { 149 | // res.statusCode = 200 150 | // res.end() 151 | // return 152 | // } 153 | 154 | // if (req.method === "GET" && parsedUrl.pathname === "/data") { 155 | // requestCounter++ 156 | 157 | // // note the time at which the request was made 158 | // firstRequestTime = firstRequestTime || new Date() 159 | // console.log( 160 | // `Request #${requestCounter} received at ${new Date().toLocaleTimeString()}, first request at ${firstRequestTime.toLocaleTimeString()}` 161 | // ) 162 | 163 | // // Mock an error for the first 5 requests 164 | // if (requestCounter <= 5) { 165 | // res.statusCode = 500 166 | // res.end("Something went wrong!") 167 | // } else { 168 | // // note the difference in the response time 169 | // const responseTime = new Date() 170 | // res.end( 171 | // `Request #${requestCounter} took ${ 172 | // Number(responseTime) - Number(firstRequestTime) 173 | // } milliseconds` 174 | // ) 175 | // } 176 | // } else { 177 | // res.statusCode = 404 178 | // res.end("Not Found") 179 | // } 180 | // }) 181 | 182 | // server.listen(3000, () => { 183 | // console.log(`Server is running on http://localhost:${3000}`) 184 | // }) 185 | -------------------------------------------------------------------------------- /examples/takeWhile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TakeWhile Demo 7 | 12 | 13 | 14 | 1 15 | 2 16 | 3 17 | 4 18 | 5 19 | 6 20 | 7 21 | 22 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/val.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Val Method Test Page 7 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 |
31 |
32 |
33 |
34 | 35 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { $, $$ } from "./core.js" 2 | import { setErrorHandler } from "./errors.js" 3 | import { promisify } from "./promisify.js" 4 | 5 | export { $, $$, promisify, setErrorHandler } 6 | -------------------------------------------------------------------------------- /methods.js: -------------------------------------------------------------------------------- 1 | import { createQueue, handlerMaker, queueAndReturn } from "./core.js" 2 | import { errorHandler, giveContext } from "./errors.js" 3 | import { fetchElements, send, wrappedFetch } from "./ajax.js" 4 | import { 5 | addStyleSheet, 6 | attach, 7 | become, 8 | Class, 9 | moveOrClone, 10 | parseArgument, 11 | setFormElementValue, 12 | } from "./DOM.js" 13 | 14 | export function addMethods(selector, target, fixed = false) { 15 | const originalTarget = [...target] 16 | 17 | let proxy = null 18 | 19 | const { addToQueue, defer } = createQueue() 20 | const queueFunction = queueAndReturn(addToQueue, () => proxy) 21 | 22 | const makeMethod = (action, context) => { 23 | return queueFunction(async (...args) => { 24 | await Promise.all(target.map((el) => action(el, ...args))) 25 | }, giveContext(context, selector)) 26 | } 27 | 28 | const customMethods = { 29 | on: makeMethod((el, ev, fn) => { 30 | el.addEventListener(ev, fn) 31 | }, "on"), 32 | 33 | once: makeMethod((el, ev, fn) => { 34 | el.addEventListener(ev, fn, { once: true }) 35 | }, "once"), 36 | 37 | delegate: makeMethod((el, ev, selector, fn) => { 38 | el.addEventListener(ev, (event) => { 39 | if (event.target.matches(selector)) { 40 | fn(event) 41 | } 42 | }) 43 | }, "delegate"), 44 | 45 | off: makeMethod((el, ev, fn) => { 46 | el.removeEventListener(ev, fn) 47 | }, "off"), 48 | 49 | html: makeMethod((el, newHtml, outer) => { 50 | if (outer) { 51 | const nextSibling = el.nextSibling // We need to get the nextSibling before removing the element 52 | el.outerHTML = newHtml // Otherwise, we lose the reference, and the proxy is empty 53 | 54 | const newElement = nextSibling // If nextSibling is null, then we're at the end of the list 55 | ? nextSibling.previousSibling // So, we get the previousSibling from where we were 56 | : el.parentElement.lastElementChild // Otherwise, we get the lastElementChild from the parent 57 | 58 | const index = target.indexOf(el) 59 | target[index] = newElement 60 | } else { 61 | el.innerHTML = newHtml 62 | } 63 | }, "html"), 64 | 65 | text: makeMethod((el, newText) => (el.textContent = newText), "text"), 66 | 67 | sanitize: makeMethod( 68 | (el, newHtml, sanitizer) => el.setHTML(newHtml, sanitizer), 69 | "sanitize" 70 | ), 71 | 72 | val: makeMethod((el, newValue) => setFormElementValue(el, newValue), "val"), 73 | 74 | css: makeMethod( 75 | (el, prop, value) => parseArgument(el.style, prop, value), 76 | "css" 77 | ), 78 | 79 | addStyleSheet: makeMethod( 80 | (_, rules) => addStyleSheet(rules), 81 | "addStyleSheet" 82 | ), 83 | 84 | addClass: makeMethod(Class("add"), "addClass"), 85 | 86 | removeClass: makeMethod(Class("remove"), "removeClass"), 87 | 88 | toggleClass: makeMethod(Class("toggle"), "toggleClass"), 89 | 90 | set: makeMethod( 91 | (el, attr, value = "") => parseArgument(el, attr, value, true), 92 | "set" 93 | ), 94 | 95 | unset: makeMethod((el, attr) => el.removeAttribute(attr), "unset"), 96 | 97 | toggle: makeMethod((el, attr) => el.toggleAttribute(attr), "toggle"), 98 | 99 | data: makeMethod( 100 | (el, key, value) => parseArgument(el.dataset, key, value), 101 | "data" 102 | ), 103 | 104 | attach: makeMethod((el, ...children) => attach(el, ...children), "attach"), 105 | 106 | cloneTo: makeMethod((el, parentSelector, options) => { 107 | moveOrClone(el, parentSelector, { mode: "clone", ...options }) 108 | }, "cloneTo"), 109 | 110 | moveTo: makeMethod((el, parentSelector, options) => { 111 | moveOrClone(el, parentSelector, options) 112 | }, "moveTo"), 113 | 114 | become: makeMethod((el, replacements, options) => { 115 | become(el, replacements, options) 116 | }, "become"), 117 | 118 | purge: makeMethod((el) => el.remove(), "purge"), 119 | 120 | send: makeMethod((el, options) => send(el, options, target), "send"), 121 | 122 | do: makeMethod(async (el, fn) => { 123 | const wrappedElement = addMethods(selector, [el]) 124 | return await fn(wrappedElement) 125 | }, "do"), 126 | 127 | defer: makeMethod((el, fn) => { 128 | const wrappedElement = addMethods(selector, [el]) 129 | return defer(fn, [wrappedElement]) 130 | }, "defer"), 131 | 132 | fromJSON: makeMethod((el, url, fn, options = {}) => { 133 | return wrappedFetch(url, options, "json", target).then((json) => { 134 | const wrappedElement = addMethods(selector, [el]) 135 | fn(wrappedElement, json) 136 | }) 137 | }, "fromJSON"), 138 | 139 | fromHTML: makeMethod((el, url, options = {}) => { 140 | return fetchElements("text", url, options, [el]) 141 | }, "fromHTML"), 142 | 143 | fromStream: makeMethod((el, url, options = {}) => { 144 | const type = options.sse ? "sse" : "stream" 145 | return fetchElements(type, url, options, [el]) 146 | }, "fromStream"), 147 | 148 | transition: makeMethod((el, keyframes, options) => { 149 | return el.animate(keyframes, options).finished 150 | }, "transition"), 151 | 152 | wait: makeMethod((el, duration) => { 153 | return new Promise((resolve) => setTimeout(resolve, duration)) 154 | }, "wait"), 155 | 156 | next: contextSwitch("nextElementSibling"), 157 | 158 | prev: contextSwitch("previousElementSibling"), 159 | 160 | first: contextSwitch("firstElementChild"), 161 | 162 | last: contextSwitch("lastElementChild"), 163 | 164 | parent: contextSwitch("parentElement"), 165 | 166 | ancestor: queueFunction((selector) => { 167 | const ancestor = filterTarget((el) => el.closest(selector)) 168 | return switchTarget(ancestor) 169 | }, giveContext("ancestor", selector)), 170 | 171 | kids: queueFunction(() => { 172 | const kidsArray = filterTarget((el) => Array.from(el.children)) 173 | return switchTarget(kidsArray.flat()) 174 | }, giveContext("kids", selector)), 175 | 176 | siblings: queueFunction(() => { 177 | const siblings = filterTarget((el) => 178 | Array.from(el.parentElement.children).filter((child) => child !== el) 179 | ) 180 | return switchTarget(siblings) 181 | }, giveContext("siblings", selector)), 182 | 183 | pick: queueFunction((selector) => { 184 | const pickedElements = filterTarget((el) => el.querySelector(selector)) 185 | return switchTarget(pickedElements) 186 | }, giveContext("pick", selector)), 187 | 188 | pickAll: queueFunction((selector) => { 189 | const pickedElements = filterTarget((el) => 190 | Array.from(el.querySelectorAll(selector)) 191 | ) 192 | return switchTarget(pickedElements.flat()) 193 | }, giveContext("pickAll", selector)), 194 | 195 | if: queueFunction( 196 | ({ is, then, or }) => { 197 | for (const el of target) { 198 | const wrappedElement = addMethods(selector, [el]) 199 | try { 200 | if (is(wrappedElement)) { 201 | then && then(wrappedElement) 202 | } else { 203 | or && or(wrappedElement) 204 | } 205 | } catch (error) { 206 | errorHandler(error, giveContext("if", selector)) 207 | } 208 | } 209 | }, 210 | giveContext("if", selector), 211 | false 212 | ), 213 | 214 | takeWhile: queueFunction((predicate, reverse) => { 215 | const result = [] 216 | if (reverse) target.reverse() 217 | for (const el of target) { 218 | try { 219 | if (predicate(el.raw || el)) { 220 | result.push(el) 221 | } else { 222 | break 223 | } 224 | } catch (error) { 225 | errorHandler(error, giveContext("takeWhile", selector)) 226 | } 227 | } 228 | return switchTarget(result) 229 | }, giveContext("takeWhile", selector)), 230 | 231 | refresh: queueFunction(() => { 232 | target = [...originalTarget] 233 | }, giveContext("refresh", selector)), 234 | } 235 | 236 | function filterTarget(action) { 237 | return target.map(action).filter((el) => el) 238 | } 239 | 240 | function switchTarget(newTarget) { 241 | if (fixed) 242 | throw new Error(`Proxy is fixed. Create new proxy to switch targets.`) 243 | target = newTarget.length > 0 ? newTarget : [] 244 | proxy = updateProxy(target) 245 | return proxy 246 | } 247 | 248 | function contextSwitch(prop) { 249 | return queueFunction(() => { 250 | const resultElements = filterTarget((el) => el[prop]) 251 | return switchTarget(resultElements) 252 | }, giveContext(prop, selector)) 253 | } 254 | 255 | function updateProxy(newTarget) { 256 | const handler = handlerMaker(newTarget, customMethods) 257 | const proxy = new Proxy(customMethods, handler) 258 | proxy.raw = newTarget 259 | return proxy 260 | } 261 | 262 | proxy = updateProxy(target) 263 | return proxy 264 | } 265 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jessquery", 3 | "version": "2.4.6", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "jessquery", 9 | "version": "2.4.6", 10 | "license": "MPL-2.0" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jessquery", 3 | "type": "module", 4 | "version": "2.4.7", 5 | "description": "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 self-documenting and works great with TypeScript.", 6 | "exports": { 7 | ".": { 8 | "types": "./index.d.ts", 9 | "default": "./index.js" 10 | } 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "jquery", 17 | "javascript", 18 | "typescript", 19 | "JSDoc", 20 | "Dom Manipulation", 21 | "Easy", 22 | "Small", 23 | "Tiny", 24 | "Modern", 25 | "ES6", 26 | "Lightweight", 27 | "Light", 28 | "DOM", 29 | "prototype", 30 | "document", 31 | "querySelector", 32 | "querySelectorAll" 33 | ], 34 | "author": "Jesse Pence", 35 | "license": "MPL-2.0", 36 | "homepage": "https://jazzypants.dev", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/jazzypants1989/jessquery.git" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /promisify.js: -------------------------------------------------------------------------------- 1 | import { errorHandler } from "./errors.js" 2 | 3 | export function promisify(fn, meta = {}) { 4 | const { timeout = 5000, interval } = meta 5 | 6 | return (...args) => 7 | new Promise((resolve, reject) => { 8 | let settled = false 9 | let intervalId 10 | 11 | const cleanUp = () => intervalId && clearInterval(intervalId) 12 | 13 | const Resolve = (value) => { 14 | if (!settled) { 15 | settled = true 16 | cleanUp() 17 | resolve(value) 18 | } 19 | } 20 | 21 | const Reject = (reason) => { 22 | if (!settled) { 23 | settled = true 24 | cleanUp() 25 | meta.error = reason 26 | reject(meta) 27 | } 28 | } 29 | 30 | const run = () => { 31 | try { 32 | fn(Resolve, Reject, ...args) 33 | } catch (e) { 34 | cleanUp() 35 | errorHandler(e, meta) 36 | } 37 | } 38 | 39 | interval ? (intervalId = setInterval(run, interval)) : run() 40 | 41 | setTimeout(() => { 42 | if (!settled) { 43 | Reject(`Timeout: ${timeout}ms exceeded`) 44 | } 45 | }, timeout) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "noEmit": true, 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext" 8 | } 9 | } 10 | --------------------------------------------------------------------------------