OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / web / static / pgv3-flash.js

116 lines · javascript ref:
// pgv3-flash.js — render a toast for ?flash=<type>:<message> query param.
//
// Closes UX gap U34: after a successful POST, the server redirects with
// ?flash=success:Created%20org%20"demo". This script reads that param
// on DOMContentLoaded, renders a dismissible toast, auto-fades after
// ~3.5s, and strips the param from the URL via history.replaceState
// (so refresh doesn't re-show it).
//
// URL format: ?flash=<type>:<message>
//   type:    success | info | error (styles the toast)
//   message: any URL-encoded text
//
// Example redirect (server):
//   /orgs/foo?flash=success:Org%20"foo"%20created
//
// The script is idempotent — multiple loads are safe, it only acts if
// the flash param is present.

(function () {
  'use strict'

  function readFlash () {
    try {
      var params = new URLSearchParams(window.location.search)
      var raw = params.get('flash')
      if (!raw) return null
      var colon = raw.indexOf(':')
      var type = colon > 0 ? raw.slice(0, colon) : 'info'
      var message = colon > 0 ? raw.slice(colon + 1) : raw
      if (!['success', 'info', 'error'].includes(type)) type = 'info'
      return { type: type, message: message }
    } catch {
      return null
    }
  }

  function stripFlashFromUrl () {
    try {
      var params = new URLSearchParams(window.location.search)
      params.delete('flash')
      var search = params.toString()
      var newUrl = window.location.pathname +
        (search ? '?' + search : '') +
        window.location.hash
      history.replaceState(null, '', newUrl)
    } catch {}
  }

  function renderToast (type, message) {
    var toast = document.createElement('div')
    toast.className = 'pgv3-flash pgv3-flash-' + type
    toast.setAttribute('role', 'status')
    toast.setAttribute('aria-live', 'polite')
    toast.innerHTML =
      '<span class="pgv3-flash-message"></span>' +
      '<button class="pgv3-flash-close" aria-label="Dismiss">×</button>'
    toast.querySelector('.pgv3-flash-message').textContent = message
    document.body.appendChild(toast)

    var dismiss = function () {
      toast.classList.add('pgv3-flash-leaving')
      setTimeout(function () {
        if (toast.parentNode) toast.parentNode.removeChild(toast)
      }, 250)
    }
    toast.querySelector('.pgv3-flash-close').addEventListener('click', dismiss)
    // Auto-dismiss after 3.5s (errors stay a bit longer — 6s).
    setTimeout(dismiss, type === 'error' ? 6000 : 3500)
  }

  // A flash can arrive via two channels:
  //   1. ?flash=<type>:<msg> on the URL — set by the server's
  //      redirectWithFlash() helper after a successful POST.
  //   2. sessionStorage['pgv3-flash-deferred'] — set by client-side
  //      code that needs to survive a window.location.reload() and
  //      doesn't have a URL to pass through (U6+U7: session-expiry
  //      re-auth + explicit sign-in). Consumed and cleared on read.
  function readDeferredFlash () {
    try {
      var raw = sessionStorage.getItem('pgv3-flash-deferred')
      if (!raw) return null
      sessionStorage.removeItem('pgv3-flash-deferred')
      var colon = raw.indexOf(':')
      var type = colon > 0 ? raw.slice(0, colon) : 'info'
      var message = colon > 0 ? raw.slice(colon + 1) : raw
      if (!['success', 'info', 'error'].includes(type)) type = 'info'
      return { type: type, message: message }
    } catch {
      return null
    }
  }

  function init () {
    // URL-borne flash wins if both are present (fresh server action
    // trumps a deferred client-side signal that's stuck in
    // sessionStorage from an earlier reload).
    var flash = readFlash()
    if (flash) {
      renderToast(flash.type, flash.message)
      stripFlashFromUrl()
      // Still clear the deferred bucket if any, so stale messages
      // don't pop up later.
      try { sessionStorage.removeItem('pgv3-flash-deferred') } catch {}
      return
    }
    var deferred = readDeferredFlash()
    if (deferred) renderToast(deferred.type, deferred.message)
  }

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