Last active
March 25, 2022 16:23
-
-
Save davidbgk/8700969263bdb9d2a31ccc1ec2328f00 to your computer and use it in GitHub Desktop.
Let's start to reference some JS utils
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// More resources: https://1loc.dev/ + https://htmldom.dev/ + https://thisthat.dev/ + https://vanillajstoolkit.com/ | |
const qsa = (selector) => Array.from(document.querySelectorAll(selector)) | |
// Another way inspired by fluorjs https://fluorjs.github.io/ | |
function $(selector, root = document) { | |
if (selector instanceof Node) { | |
return [selector] | |
} | |
return root.querySelectorAll(selector) | |
} | |
function $$(selector, root = document) { | |
if (selector instanceof Node) { | |
return selector | |
} | |
return root.querySelector(selector) | |
} | |
// With exposure | |
$: (selector, root = rootNode) => $(selector, root) | |
$$: (selector, root = rootNode) => $$(selector, root) | |
const currentAnchor = () => document.location.hash ? document.location.hash.slice(1) : '' | |
async function copyToClipboard (codeElement, alert) => { | |
try { | |
await navigator.clipboard.writeText(codeElement.innerText) | |
alert.innerHTML = `<div class="code-block__alert">Code copied!</div>` | |
// Reset the alert element after 3 seconds, | |
// which should be enough time for folks to read | |
setTimeout(() => { | |
alert.innerHTML = '' | |
}, 3000); | |
} catch (ex) { | |
alert.innerHTML = '' | |
} | |
} | |
delete(event) { | |
if (window.confirm('Êtes-vous sûr·e de cette suppression ?')) { | |
return | |
} else { | |
event.preventDefault() | |
} | |
} | |
const ready = function (cb) { | |
document.readyState === 'loading' | |
// The document is still loading | |
? document.addEventListener('DOMContentLoaded', function (e) { | |
cb() | |
}) | |
// The document is loaded completely | |
: cb() | |
} | |
ready(function() { | |
// Do something when the document is ready | |
... | |
}) | |
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches | |
if (prefersReducedMotion !== true) { | |
// do animation | |
} | |
// Extracted from https://framagit.org/drone/cardinal/-/blob/master/js/main.js | |
// Allows you to get the URL from a `fetch` error (non trivial…) | |
function request (url, options) { | |
return fetch(url, options) | |
.then(response => [response, response.json()]) | |
.then(([response, data]) => { | |
if (response.ok) { | |
return data | |
} else { | |
const e = new ServerError(`${url} ${response.status} ${data}`) | |
e.status = response.status | |
e.url = url | |
e.data = data | |
throw e | |
} | |
}) | |
.catch(error => { | |
if (error instanceof ServerError) throw error | |
const e = new Error(`${error.message} ${url}`) | |
e.url = url | |
throw e | |
}) | |
} | |
function handleError (error) { | |
console.error(error) | |
const errorURL = new window.URL(error.url) | |
const userMessage = ` | |
Le domaine ${errorURL.host} semble être inaccessible. | |
Nous en avons été informés, veuillez réessayer plus tard. | |
` | |
} | |
// For native fonction (not supported in IE but polyfill exists): | |
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams | |
function toQueryParams (data) { | |
return Object.keys(data).map(k => `${k}=${data[k]}`).join('&') | |
} | |
function getBBox (map) { | |
const bounds = map.getBounds() | |
return { | |
west: bounds.getWest(), | |
south: bounds.getSouth(), | |
east: bounds.getEast(), | |
north: bounds.getNorth() | |
} | |
} | |
function utcIsoString (dateStr) { | |
return (new Date(new Date(dateStr || Date.now()).toUTCString())).toISOString() | |
} | |
function fromQueryParams () { | |
return new Map(document.location.search.slice(1).split('&').map(kv => kv.split('='))) | |
} | |
function mapToDict(map) { | |
const dict = {} | |
map.forEach((v, k) => { dict[k] = v }) | |
return dict | |
} | |
// Usage: | |
const data = { | |
lat: lat, | |
lon: lng, | |
alt: 0 | |
} | |
request(`${this.config.checkURL}?${toQueryParams(data)}`) | |
.then(response => { /* do something */}) | |
.catch(handleError) | |
// Another request/response binding for fetch (from Hey!), splitted across many files. | |
// Kudos to them for sharing their source files from https://app.hey.com/sign_up/welcome | |
// lib/http/request.js | |
import { Response } from "./response" | |
import { getCookie } from "lib/cookie" | |
export class Request { | |
constructor(method, url, options = {}) { | |
this.method = method | |
this.url = url | |
this.options = options | |
} | |
async perform() { | |
const response = new Response(await fetch(this.url, this.fetchOptions)) | |
if (response.unauthenticated && response.authenticationURL) { | |
return Promise.reject(window.location.href = response.authenticationURL) | |
} else { | |
return response | |
} | |
} | |
get fetchOptions() { | |
return { | |
method: this.method, | |
headers: this.headers, | |
body: this.body, | |
signal: this.signal, | |
credentials: "same-origin", | |
redirect: "follow" | |
} | |
} | |
get headers() { | |
return compact({ | |
"X-Requested-With": "XMLHttpRequest", | |
"X-CSRF-Token": this.csrfToken, | |
"Content-Type": this.contentType, | |
"Accept": this.accept | |
}) | |
} | |
get csrfToken() { | |
const csrfParam = document.head.querySelector("meta[name=csrf-param]")?.content | |
return csrfParam ? getCookie(csrfParam) : undefined | |
} | |
get contentType() { | |
if (this.options.contentType) { | |
return this.options.contentType | |
} else if (this.body == null || this.body instanceof FormData) { | |
return undefined | |
} else if (this.body instanceof File) { | |
return this.body.type | |
} else { | |
return "application/octet-stream" | |
} | |
} | |
get accept() { | |
switch (this.responseKind) { | |
case "html": | |
return "text/html, application/xhtml+xml" | |
case "json": | |
return "application/json" | |
default: | |
return "*/*" | |
} | |
} | |
get body() { | |
return this.options.body | |
} | |
get responseKind() { | |
return this.options.responseKind || "html" | |
} | |
get signal() { | |
return this.options.signal | |
} | |
} | |
function compact(object) { | |
const result = {} | |
for (const key in object) { | |
const value = object[key] | |
if (value !== undefined) { | |
result[key] = value | |
} | |
} | |
return result | |
} | |
// lib/http/response.js | |
class _Response { | |
constructor(response) { | |
this.response = response | |
} | |
get statusCode() { | |
return this.response.status | |
} | |
get ok() { | |
return this.response.ok | |
} | |
get unauthenticated() { | |
return this.statusCode == 401 | |
} | |
get authenticationURL() { | |
return this.response.headers.get("WWW-Authenticate") | |
} | |
get contentType() { | |
const contentType = this.response.headers.get("Content-Type") || "" | |
return contentType.replace(/;.*$/, "") | |
} | |
get headers() { | |
return this.response.headers | |
} | |
get html() { | |
if (this.contentType.match(/^(application|text)\/(html|xhtml\+xml)$/)) { | |
return this.response.text() | |
} else { | |
return Promise.reject(`Expected an HTML response but got "${this.contentType}" instead`) | |
} | |
} | |
get json() { | |
if (this.contentType.match(/^application\/json/)) { | |
return this.response.json() | |
} else { | |
return Promise.reject(`Expected a JSON response but got "${this.contentType}" instead`) | |
} | |
} | |
get text() { | |
return this.response.text() | |
} | |
} | |
export { _Response as Response } | |
// helpers/request_helpers.js | |
import { Request } from "lib/http" | |
export async function request(method, url, options) { | |
const request = new Request(method, url, options) | |
const response = await request.perform() | |
if (!response.ok) throw new Error(response.statusCode) | |
return request.responseKind == "json" | |
? response.json | |
: response.text | |
} | |
[ "get", "post", "put", "delete" ].forEach(method => { | |
request[method] = (...args) => request(method, ...args) | |
}) | |
request.getJSON = (url, options = {}) => request.get(url, { responseKind: "json", ...options }) | |
// lib/cookie.js | |
export function getCookie(name) { | |
const cookies = document.cookie ? document.cookie.split("; ") : [] | |
const prefix = `${encodeURIComponent(name)}=` | |
const cookie = cookies.find(cookie => cookie.startsWith(prefix)) | |
if (cookie) { | |
const value = cookie.split("=").slice(1).join("=") | |
return value ? decodeURIComponent(value) : undefined | |
} | |
} | |
const twentyYears = 20 * 365 * 24 * 60 * 60 * 1000 | |
export function setCookie(name, value) { | |
const body = [ name, value ].map(encodeURIComponent).join("=") | |
const expires = new Date(Date.now() + twentyYears).toUTCString() | |
const cookie = `${body}; path=/; expires=${expires}` | |
document.cookie = cookie | |
} | |
// Another implementation of `fetch` (this thing is truly half-baked) from | |
// https://framagit.org/drone/raccoon/-/blob/master/js/request.js | |
// Inspired by https://github.com/zellwk/zl-fetch | |
// See https://css-tricks.com/using-fetch/ | |
function request (url, options = undefined) { | |
return fetch(url, optionsHandler(options)) | |
.then(handleResponse) | |
.catch(error => { | |
const e = new Error(`${error.message} ${url}`) | |
Object.assign(e, error, {url}) | |
throw e | |
}) | |
} | |
function optionsHandler (options) { | |
const def = { | |
method: 'GET', | |
headers: {'Content-Type': 'application/json'} | |
} | |
if (!options) return def | |
let r = Object.assign({}, def, options) | |
// Deal with body, can be either a hash or a FormData, | |
// will generate a JSON string from it if in options. | |
delete r.body | |
if (options.body) { | |
// Allow to pass an empty hash too. | |
if (!(Object.getOwnPropertyNames(options.body).length === 0)) { | |
r.body = JSON.stringify(options.body) | |
} else if (options.body instanceof FormData) { | |
r.body = JSON.stringify(Array.from(options.body.entries())) | |
} | |
} | |
return r | |
} | |
const handlers = { | |
JSONResponseHandler (response) { | |
return response.json() | |
.then(json => { | |
if (response.ok) { | |
return json | |
} else { | |
return Promise.reject(Object.assign({}, json, { | |
status: response.status | |
})) | |
} | |
}) | |
}, | |
textResponseHandler (response) { | |
if (response.ok) { | |
return response.text() | |
} else { | |
return Promise.reject(Object.assign({}, { | |
status: response.status, | |
message: response.statusText | |
})) | |
} | |
} | |
} | |
function handleResponse (response) { | |
let contentType = response.headers.get('content-type') | |
if (contentType.includes('application/json')) { | |
return handlers.JSONResponseHandler(response) | |
} else if (contentType.includes('text/html')) { | |
return handlers.textResponseHandler(response) | |
} else { | |
throw new Error(` | |
Sorry, content-type '${contentType}' is not supported, | |
only 'application/json' and 'text/html' are. | |
`) | |
} | |
} | |
/* To build some kind of a plugins system: | |
https://github.com/simonw/datasette/issues/9 | |
See also https://simonwillison.net/2021/Jan/3/weeknotes/ | |
*/ | |
var datasette = datasette || {}; | |
datasette.plugins = (() => { | |
var registry = {}; | |
return { | |
register: (hook, fn) => { | |
registry[hook] = registry[hook] || []; | |
registry[hook].push(fn); | |
}, | |
call: (hook, args) => { | |
var results = (registry[hook] || []).map(fn => fn(args||{})); | |
return results; | |
} | |
}; | |
})(); | |
// And the register/call like this: | |
datasette.plugins.register('numbers', ({a, b}) => a + b) | |
datasette.plugins.register('numbers', o => o.a * o.b) | |
datasette.plugins.call('numbers', {a: 4, b: 6}) | |
/* Preventing double form submissions: | |
https://til.simonwillison.net/javascript/preventing-double-form-submission | |
*/ | |
function protectForm(form) { | |
var locked = false | |
form.addEventListener('submit', (ev) => { | |
if (locked) { | |
ev.preventDefault() | |
return | |
} | |
locked = true | |
setTimeout(() => { | |
locked = false | |
}, 2000) | |
}) | |
} | |
window.addEventListener('load', () => { | |
Array.from(document.querySelectorAll('form')).forEach(protectForm) | |
}) | |
/* Maybe a better pattern to match even created forms (better perfs too): | |
Inspired by https://gomakethings.com/why-event-delegation-is-a-better-way-to-listen-for-events-in-vanilla-js/ | |
*/ | |
document.addEventListener('click', (event) => { | |
if (event.target.matches('form, form *')) { | |
// Run your code to prevent double submission | |
} | |
}, false) | |
// To respect users' preferences regarding motion. | |
function getAnimationBehavior() { | |
const isReduced = | |
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || | |
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true | |
return isReduced ? 'auto' : 'smooth' | |
} | |
// One possible usage: | |
function scrollToSummary() { | |
const details = document.querySelectorAll('details') | |
Array.from(details).forEach((detail) => { | |
detail.addEventListener('click', (event) => { | |
// Even with an event, we need to wait for the next few | |
// ticks to be able to scroll to the collapsed element. | |
setTimeout(() => { | |
event.target.scrollIntoView({ behavior: getAnimationBehavior() }) | |
}, 100) | |
}) | |
}) | |
} | |
// Use FormData to serialize your payload for fetch() | |
// https://www.baldurbjarnason.com/2021/fetch-and-formdata/ | |
const formdata = new FormData(form) | |
fetch('/test/thing', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/x-www-form-urlencoded' | |
}, | |
body: new URLSearchParams(formdata).toString() | |
}).then(result => { | |
// do something | |
}).catch(err => { | |
// fix something. | |
}) | |
// Same with sending JSON: | |
const formdata = new FormData(form) | |
fetch('/test/thing', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.serialise(Object.fromEntries(formdata.entries())) | |
}).then(result => { | |
// do something | |
}).catch(err => { | |
// fix something. | |
}) | |
// How to check if an API error response is JSON or not | |
// https://gomakethings.com/how-to-check-if-an-api-error-response-is-json-or-not-with-vanilla-javascript/ | |
fetch('https://jsonplaceholder.typicode.com/tododos').then(function (response) { | |
if (response.ok) { | |
return response.json(); | |
} | |
throw response; | |
}).then(function (data) { | |
console.log(data); | |
}).catch(function (error) { | |
// Check if the response is JSON or not | |
let isJSON = error.headers.get('content-type').includes('application/json'); | |
// If JSON, use text(). Otherwise, use json(). | |
let getMsg = isJSON ? error.json() : error.text(); | |
// Warn the error and message when it resolves | |
getMsg.then(function (msg) { | |
console.warn(error, msg); | |
}); | |
}); | |
function slugify(str) { | |
/* Adapted from | |
https://mhagemann.medium.com/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1 */ | |
const a = | |
'àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;' | |
const b = | |
'aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------' | |
const p = new RegExp(a.split('').join('|'), 'g') | |
return str | |
.toString() | |
.toLowerCase() | |
.replace(/\s+/g, '-') // Replace spaces with - | |
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters | |
.replace(/’/g, "'") // Turn apostrophes to single quotes | |
.replace(/[^a-zA-Z0-9-']+/g, '') // Remove all non-word characters except single quotes | |
.replace(/--+/g, '-') // Replace multiple - with single - | |
.replace(/^-+/, '') // Trim - from start of text | |
.replace(/-+$/, '') // Trim - from end of text | |
} | |
/** | |
* Get a template from a string | |
* https://stackoverflow.com/a/41015840 | |
* https://gomakethings.com/html-templates-with-vanilla-javascript/#a-hybrid-approach | |
* @param {String} str The string to interpolate | |
* @param {Object} params The parameters | |
* @return {String} The interpolated string | |
*/ | |
function interpolate (str, params) { | |
let names = Object.keys(params); | |
let vals = Object.values(params); | |
return new Function(...names, `return \`${str}\`;`)(...vals); | |
} | |
` | |
<template id="list-item"> | |
<div class="wizard"> | |
<strong>${wizard}</strong> | |
</div> | |
</template> | |
` | |
// Create an HTML string | |
let html = ''; | |
// Loop through each wizard | |
for (let wizard of wizards) { | |
html += interpolate(listItem.innerHTML, {wizard}); | |
} | |
// Add the HTML to the UI | |
app.innerHTML = html; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
An approach that might be useful to avoid polluting builtins: https://github.com/DavidBruant/tiretbas-natives