Skip to content

Instantly share code, notes, and snippets.

@davidbgk
Last active March 25, 2022 16:23
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidbgk/8700969263bdb9d2a31ccc1ec2328f00 to your computer and use it in GitHub Desktop.
Save davidbgk/8700969263bdb9d2a31ccc1ec2328f00 to your computer and use it in GitHub Desktop.
Let's start to reference some JS utils
// 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;
@davidbgk
Copy link
Author

davidbgk commented Oct 1, 2020

An approach that might be useful to avoid polluting builtins: https://github.com/DavidBruant/tiretbas-natives

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment