Aller au contenu

JavaScript⚓︎

Raccourci pour sélectionner une liste de nœuds⚓︎

🐣 2022-08

La méthode standard qui consiste à utiliser querySelectorAll() est relativement verbeuse et ne permet pas de faire facilement un forEach() dessus. Cette version simplifiée est un raccourci utile lorsqu’on doit le faire souvent :

qsa.js
1
2
3
function qsa(selector, root = document) {
  return Array.from(root.querySelectorAll(selector)) // (1)!
}
  1. On en profite pour retourner un Array !

Récupérer la chaîne de l’ancre d’une URL⚓︎

🐣 2022-08

Je passe toujours trop de temps à retrouver comment faire.

anchor.js
1
2
3
function currentAnchor() {
  return document.location.hash ? document.location.hash.slice(1) : ''
}

Prendre en compte les préférences utilisateur·ice pour lancer une animation⚓︎

🐣 2022-08

Très important car des personnes sont mal à l’aise avec certaines animations (j’en fait partie, parfois).

motion.js
1
2
3
4
5
6
7
const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches

if (prefersReducedMotion !== true) {
  // do animation
}

Voir aussi ce qu’il est possible de faire en HTML et en CSS.

Raccourci pour créer/attacher un évènement⚓︎

🐣 2022-08

Un bout de code qui vient de Chris Ferdinandi (voir 1, 2 et 3) :

emit.js
1
2
3
4
5
6
7
8
function emit(type, detail, elem = document) {
  let event = new CustomEvent(type, {
    bubbles: true,
    cancelable: true,
    detail: detail,
  })
  return elem.dispatchEvent(event)
}

Il peut s’utiliser ainsi emit('calculator:add', {total: 20}, calendarElement) et s’écouter de manière classique.

Polyfills à la demande⚓︎

🐣 2022-08

Il est possible d’importer des polyfills externes directement depuis une balise <script>, par exemple pour Promise ou fetch :

polyfills.html
<script>
  window.Promise ||
    document.write(
      '<script src="https://unpkg.com/es6-promise@4.2.8/dist/es6-promise.auto.min.js"><\/script>'
    )
  window.fetch ||
    document.write(
      '<script src="https://unpkg.com/whatwg-fetch@3.6.2/dist/fetch.umd.js"><\/script>'
    )
</script>

En pratique, c’est quand même mieux de ne pas faire des requêtes externes et d’avoir les fichiers en local.

Copier dans le presse-papier⚓︎

🐣 2022-08

Lorsqu’il y a un champ avec un code ou une URL à copier-coller, ça peut être intéressant de proposer à l’utilisateur·ice de cliquer pour mettre l’information dans son presse-papier et le coller ailleurs.

copy-clipboard.js
function copyToClipboard(codeElement, alert) {
  try {
    navigator.clipboard.writeText(codeElement.innerText)
    alert.innerHTML = `<div class="custom-class">Code copied!</div>`
    setTimeout(() => {
      alert.innerHTML = ''
    }, 3000) // (1)
  } catch (ex) {
    alert.innerHTML = ''
  }
}
  1. Vous pouvez adapter cette valeur qui correspond au temps d’affichage de l’information (en millisecondes).

Confirmation de suppression⚓︎

🐣 2022-08

La façon la plus simple d’ouvrir une popup de confirmation.

delete-confirmation.js
1
2
3
4
5
6
7
function deleteConfirmation(event) {
  if (window.confirm('Are you sure you want to delete this?')) {
    return
  } else {
    event.preventDefault()
  }
}

Fermeture automatique

Notez qu’à force de recevoir des notifications, les utilisateur·ices sont promptes à fermer une fenêtre, aussi rouge soit-elle. Il peut-être pertinent de demander une confirmation qui demande de saisir quelque chose comme le fait Github lorsqu’on supprime un dépôt (il faut taper le nom complet du dépôt concerné).

Générer un slug⚓︎

🐣 2022-08

Pour transformer un titre en français en une portion d’URL par exemple. Adapté depuis cet article.

slugify.js
function slugify(str) {
  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
}

Un système de plugins⚓︎

🐣 2022-08

Ce fragment est issu de ce qu’à fait Simon Willison pour Datasette, aidé par Matthew Somerville.

plugins.js
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
    },
  }
})()

Il s’utilise ensuite ainsi :

1
2
3
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})

Paramètres par défaut⚓︎

🐣 2022-08

Il est possible de définir des paramètres par défaut en utilisant la syntaxe suivante :

parameters.js
1
2
3
function checkName({ name = 'Castor' } = {}) {
  console.log({ name })
}

Il est aussi possible d’avoir un comportement similaires aux **kwargs en Python grâce au rest parameter :

parameters-kwargs.js
1
2
3
function checkNameWithKwargs({ name = 'Pollux', ...kwargs } = {}) {
  console.log({ name, ...kwargs })
}

Cela nécessite d’appeler la fonction avec un objet (ce qui pourrait être une bonne pratique ?).

checkNameWithKwargs({name: 'Télaïre', tessiture:'soprano'})

Télécharger un fichier dynamiquement⚓︎

🐣 2022-09

Parfois, on veut pouvoir télécharger un fichier généré dynamiquement en JavaScript, par exemple ici pour transformer un tableau en un export CSV (la récupération des données n’est pas montrée).

download-file.js
function downloadAsCSV(event) {
  event.preventDefault()
  const content = 'foo;bar' // (1)!
  const name = 'baz_quux'
  const filename = `export_${new Date().toLocaleDateString()}_${name}.csv` // (2)!
  const csvFile = new Blob([content], { type: 'text/csv' }) // (3)!
  const fakeDownloadLink = document.createElement('a') // (4)!
  fakeDownloadLink.download = filename // (5)
  fakeDownloadLink.href = window.URL.createObjectURL(csvFile) // (6)!
  fakeDownloadLink.style.display = 'none'
  document.body.appendChild(fakeDownloadLink)
  fakeDownloadLink.click() // (7)!
  fakeDownloadLink.remove()
}
  1. Le contenu souhaité du fichier à aller récupérer par ailleurs
  2. C’est toujours intéressant d’avoir la date dans des fichiers d’export
  3. Création d’un Blob simulant le fichier
  4. Création du faux lien qui va nous servir à simuler le téléchargement
  5. Utilisation de l’attribut download qui permet de donner un nom au fichier téléchargé
  6. Création d’une URL contenant le contenu du fichier
  7. Simulation du clic avant la suppression de l’élément pour lancer le téléchargement

Enlever des espaces⚓︎

🐣 2022-09

Il est très courant de vouloir enlever des espaces et sauts de lignes (depuis un element.textContent par exemple).

strip-extra-spaces.js
const stripExtraSpaces = (str) => str.replace(/\r?\n|\r|\s\s+/g, '')

Avoir une méthode range()⚓︎

🐣 2022-11

Un moyen d’avoir une méthode range() qui ait le même comportement qu’en Python, décrite en détail par Kieran Barker et adaptée par Chris Ferdinandi.

range.js
1
2
3
4
5
6
7
8
9
function range(stop, start = 1, step = 1) {
  return Array.from(
    { length: (stop - start) / step + 1 },
    (_, i) => start + i * step
  )
}
// range(5) => [ 1, 2, 3, 4, 5 ]
// range(10, 8) => [ 8, 9, 10 ]
// range(10, 1, 3) => [ 1, 4, 7, 10 ]

Avoir une méthode zip()⚓︎

🐣 2022-11

Un vieux truc que j’avais récupéré sur StackOverflow qui est quasiment un équivalent de la méthode zip() en Python.

zip.js
1
2
3
4
function zip(rows) {
  return rows[0].map((_, index) => rows.map((row) => row[index]))
}
// zip([['foo', 'bar'], [1, 2]]) => [['foo', 1], ['bar', 2]]

Utiliser des loggers personnalisés⚓︎

🐣 2022-11

Il est possible de définir ses propres loggers pour s’y retrouver plus facilement dans la console. C’est notamment utile si vous faites une lib ou un module réutilisable.

logger.js
1
2
3
4
5
function logger(scope, level = 'log', color = 'green') {
  return (...args) => {
    console[level]('%c%s', `color:${color}`, scope, ...args)
  }
}

Cela peut s’utiliser ensuite ainsi (très adaptable en fonction de votre sensibilité) :

1
2
3
const log = logger('myapp')
const logerr = logger('myapp', 'error', 'red')
const logdev = logger('myapp', 'debug', 'blue')

Et puis dans votre code enfin :

1
2
3
log('page initialized')
logerr('oops')
logdev('wtf')

Je vous laisse vous amuser dans la console :).

🐫 Du camel au kebab⚓︎

🐣 2022-11

Toute petite fonction mais bien pratique pour convertir des noms de variables en data-attributes par exemple !

camel-to-kebab.js
1
2
3
function camelToKebab(s) {
  return s.replace(/[A-Z]/g, (s) => '-' + s.toLowerCase())
}

Raccourcis pour les attributs⚓︎

🐣 2022-11

Et là vous allez comprendre pourquoi la précédente fonction était utile.

attributes.js
function attr(el, name, value = undefined) {
  const curValue = el.getAttribute(name)
  if (typeof name === 'object') {
    for (const at in name) {
      el.setAttribute(camelToKebab(at), name[at]) // (1)!
    }
    return null
  } else if (value === undefined) return el.getAttribute(name)
  else if (value === null) return el.removeAttribute(name), curValue
  else return el.setAttribute(name, value), value
}
  1. Nécessite la fonction camelToKebab() définie juste au-dessus.

Cela s’utilise ainsi :

1
2
3
4
5
attr(el, 'foo') // récupère l’attribut `foo` de `el`
attr(el, 'bar', 'baz') // l’attribut `bar` passe à `baz`
attr(el, 'quux', null) // retrait de l’attribut `quux`
attr(el, [ dataFoo: 'valueBar', dataBaz: 'valueQuux' ])
// set l’attribut `data-foo` à `valueBar` et `data-baz` à `valueQuux`

Chargement lorsque le DOM est prêt⚓︎

🐣 2022-11

Il y pas mal de façon de le faire mais ça revient un peu toujours au même. J’aime bien que ce soit explicite et que ça prenne soin de retirer l’écouteur de l’évènement à la fin.

dom-ready.js
function onload() {
  // An example of what you might want to do:
  var root = document.documentElement
  root.classList.remove('no-js')

  // Do something!

  // Clean up
  document.removeEventListener('DOMContentLoaded', onload)
}

if (document.readyState != 'loading') {
  onload()
} else {
  document.addEventListener('DOMContentLoaded', onload)
}

Stocker des informations dans l’URL⚓︎

🐣 2023-01

Une façon de stocker des données ou un état dans un hash d’URL partagée par Scott Antipa.

store-in-url.js
const stateString = JSON.stringify(appState) // appState is a json object
const compressed = compress(stateString)
const encoded = Base64.encode(compressed)
// Push that `encoded` string to the url
// ... Later, on page load or on undo/redo we read the url and
// do the following
const decoded = Base64.decode(encoded) // same encoded as above, but read from url
const uncompressed = uncompress(decoded)
const newState = JSON.parse(uncompressed)
// Now load your application with the newState

Il faut par contre une bibliothèque de dé·compression en complément, comme pako (la partie compress/uncompress).

Générer des chemins relatifs pour des fichiers statiques de modules⚓︎

🐣 2023-01

Découvert via Going Buildless de Modern Web.

modules-relative-paths.js
1
2
3
4
const imgSrc = new URL('./asset.webp', import.meta.url)
const image = document.createElement('img')
image.src = imgSrc.href
document.body.appendChild(image)

L’utilisation de la valeur un peu magique de import.meta.url va permettre de générer une URL relative au fichier JS concerné, ce qui permet de garder les fichiers statiques proches du code. Intéressant pour une approche composants/modulaire.

🧷 Échapper le contenu de variables au rendu⚓︎

🐣 2023-01

Découvert en analysant le code d’un plugin datasette.

autoescape.js
function htmlEscape(html) {
  return html
    .replace(/&/g, '&amp;')
    .replace(/>/g, '&gt;')
    .replace(/</g, '&lt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
}

class Safe extends String {}

function safe(s) {
  if (!(s instanceof Safe)) {
    return new Safe(s)
  } else {
    return s
  }
}

const autoescape = (fragments, ...inserts) =>
  safe(
    fragments
      .map((fragment, i) => {
        let insert = inserts[i] || ''
        if (!(insert instanceof Safe)) {
          insert = htmlEscape(insert.toString())
        }
        return fragment + insert
      })
      .join('')
  )

Ça s’utilise ensuite ainsi (en imaginant que meh ait été saisi par l’utilisateur·ice) :

1
2
3
const meh = "<script>"
autoescape`foo ${meh} bar`
// => String { "foo &lt;script&gt; bar" }

Retirer les tags HTML d’une chaîne⚓︎

🐣 2023-11

Récupéré depuis cet article (ce site contient beaucoup d’astuces du genre !).

striptags-template.js
1
2
3
4
5
function stripTags(html) {
  const template = document.createElement('template')
  template.innerHTML = html
  return template.content.textContent || ''
}

On crée un élément template (vs. div), ce qui évite d’exécuter les potentiels scripts inconvenants qui pourraient se trouver dans le HTML transmis. Simple et efficace.

La version consistant à utiliser DOMParser est intéressante aussi :

striptags-domparser.js
1
2
3
4
function stripTags(html) {
  const doc = new DOMParser().parseFromString(html, 'text/html')
  return doc.body.textContent || ''
}

Pré-remplir un formulaire avec les paramètres de l’URL⚓︎

🐣 2023-04

Il est souvent pratique de pouvoir faire un lien vers un formulaire en ayant pré-remplis certains champs /mon-form/?author=Bob&accept=1.

La solution suivante pré-suppose que chaque input ait un attribut name.

prefill-form.js
for (const [key, value] of new URL(window.location.href).searchParams) {
  const elements = document.getElementsByName(key)
  for (const element of elements) {
    if (['checkbox', 'radio'].includes(element.type)) {
      if (element.value == value) {
        element.checked = true
      }
    } else if (element.multiple) {
      for (const option of element.options) {
        if (option.value == value) {
          option.selected = true
        }
      }
    } else {
      element.value = value
    }
  }
}

Rechercher des mots dans un texte⚓︎

🐣 2023-10

Un travail que l’on avait commencé ensemble avec Anthony pour mon blog et que j’ai adapté et réutilisé récemment.

search-text.html
<script id="search-stop-words" type="application/json">
  ["stop", "words"]  // https://github.com/stopwords-iso/stopwords-fr
</script>

<script>
  const stopWords = JSON.parse(
    document.getElementById('search-stop-words').textContent  // (1)
  )

  function matchSearch(needle, stack) {
    // Create a regex for each word from the needle
    const regMap = needle
      .toLowerCase()
      .split(' ')
      .filter((word) => word.length && !stopWords.includes(word))
      // See https://stackoverflow.com/a/37511463
      .map((word) => word.normalize("NFD").replace(/[\u0300-\u036f]/g, ""))
      .map((word) => new RegExp(word, 'i'))
    let matches = false
    for (let reg of regMap) {
      matches = stack.match(reg)
    }
    return matches
  }
</script>
  1. Je documente aussi la logique de mettre les stop words dans le HTML en JSON puis de les parser pour les récupérer car c’est la manière la plus performante de faire.

Voir aussi cette entrée Python et les suivantes pour avoir la logique côté serveur.

Pour aller plus loin/différemment⚓︎