gregschmit/rails-rest-framework

View on GitHub
app/views/rest_framework/_head.html.erb

Summary

Maintainability
Test Coverage
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<%= render partial: "rest_framework/head/external" %>

<style>
  :root {
    --rrf-red: #900;
    --rrf-red-hover: #5f0c0c;
    --rrf-light-red: #db2525;
    --rrf-light-red-hover: #b80404;
  }
  #rrfAccentBar {
    background-color: var(--rrf-red);
    height: .3em;
    width: 100%;
    margin: 0;
    padding: 0;
  }
  header nav { background-color: black; }

  /* Header adjustments. */
  h1 { font-size: 2rem; }
  h2 { font-size: 1.7rem; }
  h3 { font-size: 1.5rem; }
  h4 { font-size: 1.3rem; }
  h5 { font-size: 1.1rem; }
  h6 { font-size: 1rem; }
  h1, h2, h3, h4, h5, h6 {
    color: var(--rrf-red);
    font-weight: normal;
  }
  html[data-bs-theme="dark"] h1,
  html[data-bs-theme="dark"] h2,
  html[data-bs-theme="dark"] h3,
  html[data-bs-theme="dark"] h4,
  html[data-bs-theme="dark"] h5,
  html[data-bs-theme="dark"] h6 {
    color: var(--rrf-light-red);
  }

  /* Improve code and code blocks. */
  pre code, .trix-content pre {
    display: block;
    overflow-x: auto;
    padding: .5em !important;
  }
  code, .trix-content pre {
    --bs-code-color: black;
    background-color: #eee !important;
    border: 1px solid #aaa;
    padding: .1em .3em;
  }
  html[data-bs-theme="dark"] code, html[data-bs-theme="dark"] .trix-content pre {
    --bs-code-color: white;
    background-color: #2b2b2b !important;
  }

  /* Anchors */
  a:not(.nav-link) {
    text-decoration: none;
    color: var(--rrf-red);
  }
  a:hover:not(.nav-link) {
    text-decoration: underline;
    color: var(--rrf-red-hover);
  }
  html[data-bs-theme="dark"] a:not(.nav-link) { color: var(--rrf-light-red); }
  html[data-bs-theme="dark"] a:hover:not(.nav-link) { color: var(--rrf-light-red-hover); }

  /* Reduce label font size. */
  label.form-label {
    font-size: .8em;
  }

  /* Make Trix buttons visible even in dark mode. */
  trix-toolbar .trix-button-group {
    background-color: #eee;
  }

  /* Make Trix dialog URL input visible in dark mode. */
  input.trix-input--dialog {
    color: black;
  }

  /* Make route group expansion obvious to the user. */
  .rrf-routes .rrf-route-group-header:hover {
    background-color: #ddd;
  }
  html[data-bs-theme="dark"] .rrf-routes .rrf-route-group-header:hover {
    background-color: #333;
  }

  /* Disable bootstrap's collapsing animation because in tables it causes delayed jerkiness. */
  .rrf-routes .collapsing {
    -webkit-transition: none;
    transition: none;
    display: none;
  }

  /* Copy-to-clipboard styles. */
  .rrf-copy {
    position: relative;
  }
  .rrf-copy .rrf-copy-link {
    position: absolute;
    top: .25em;
    right: .4em;
    transition: 0.3s ease;
    font-size: 1.5em;
  }
  .rrf-copy .rrf-copy-link.rrf-clicked {
    color: green;
  }
  .rrf-copy .rrf-copy-link.rrf-clicked:hover {
    color: green;
  }
</style>

<script>
  // Javascript for dark/light mode.
  ;(() => {
    // Get the real mode from a selected mode. Anything other than "light" or "dark" is treated as
    // "system" mode.
    const rrfGetRealMode = (selectedMode) => {
      if (selectedMode === "light" || selectedMode === "dark") {
        return selectedMode
      }

      if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
        return "dark"
      }

      return "light"
    }

    // Set the mode, given a "selected" mode.
    const rrfSetSelectedMode = (selectedMode) => {
      // Anything except "light" or "dark" is casted to "system".
      if (selectedMode !== "light" && selectedMode !== "dark") {
        selectedMode = "system"
      }

      // Store selected mode in `localStorage`.
      localStorage.setItem("rrfMode", selectedMode)

      // Set the mode selector to the selected mode.
      const modeComponent = document.getElementById("rrfModeComponent")
      if (modeComponent) {
        let labelHTML
        modeComponent.querySelectorAll("button[data-rrf-mode-value]").forEach((el) => {
          if (el.getAttribute("data-rrf-mode-value") === selectedMode) {
            el.classList.add("active")
            labelHTML = el.querySelector("i").outerHTML.replace("ms-2", "me-1")
          } else {
            el.classList.remove("active")
          }
        })
        modeComponent.querySelector("button[data-bs-toggle]").innerHTML = labelHTML
      }

      // Get the real mode to use.
      realMode = rrfGetRealMode(selectedMode)

      // Set the `realMode` effects.
      if (realMode === "light") {
        document.querySelectorAll(".rrf-light-mode").forEach((el) => {
          el.disabled = false
        })
        document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
          el.disabled = true
        })
        document.querySelectorAll(".rrf-mode").forEach((el) => {
          el.setAttribute("data-bs-theme", "light")
        })
      } else if (realMode === "dark") {
        document.querySelectorAll(".rrf-light-mode").forEach((el) => {
          el.disabled = true
        })
        document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
          el.disabled = false
        })
        document.querySelectorAll(".rrf-mode").forEach((el) => {
          el.setAttribute("data-bs-theme", "dark")
        })
      } else {
        console.log(`RRF: Unknown mode: ${mode}`)
      }
    }

    // Initialize dark/light mode before page fully loads to prevent flash.
    rrfSetSelectedMode(localStorage.getItem("rrfMode"))

    // Initialize dark/light mode after page load (duplicate so mode component is updated).
    document.addEventListener("DOMContentLoaded", (event) => {
      rrfSetSelectedMode(localStorage.getItem("rrfMode"))

      // Also set up mode selector.
      document.querySelectorAll("#rrfModeComponent button[data-rrf-mode-value]").forEach((el) => {
        el.addEventListener("click", (event) => {
          rrfSetSelectedMode(event.target.getAttribute("data-rrf-mode-value"))
        })
      })
    })

    // Handle case where user changes system theme.
    if (window.matchMedia) {
      window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
        const selectedMode = localStorage.getItem("rrfMode")
        if (selectedMode !== "light" && selectedMode !== "dark") {
          rrfSetSelectedMode("system")
        }
      })
    }
  })();

  document.addEventListener("DOMContentLoaded", (event) => {
    // Pretty-print JSON.
    document.querySelectorAll(".language-json").forEach((el) => {
      el.innerHTML = neatJSON(JSON.parse(el.innerText), {
        wrap: 80,
        afterComma: 1,
        afterColon: 1,
      }).replaceAll("&", "&amp;")
        .replaceAll("<", "&lt;")
        .replaceAll(">", "&gt;")
        .replaceAll('"', "&quot;")
        .replaceAll("'", "&#039;")
    })

    // Then highlight it.
    hljs.configure({cssSelector: "pre code"})
    hljs.highlightAll()

    // Replace text node links with anchor tag links.
    document.querySelectorAll(".rrf-copy code").forEach((el) => {
      el.innerHTML = rrfLinkify(el.innerHTML)
    })

    // Insert copy links.
    document.querySelectorAll(".rrf-copy").forEach((el) => {
      el.insertAdjacentHTML(
        "afterbegin",
        '<a class="rrf-copy-link" title="Copy to Clipboard" href="#"><i class="bi bi-clipboard-fill"></i></a>',
      )
    })

    // Copy link implementation.
    document.querySelectorAll(".rrf-copy-link").forEach((el) => {
      el.addEventListener("click", (event) => {
        const range = document.createRange()
        range.selectNode(el.nextSibling)
        window.getSelection().removeAllRanges()
        window.getSelection().addRange(range)
        if (document.execCommand("copy")) {
          // Trigger clicked animation.
          el.classList.add("rrf-clicked")
          el.innerHTML = '<i class="bi bi-clipboard-check-fill">'
          setTimeout(() => {
            el.classList.remove("rrf-clicked")
            el.innerHTML = '<i class="bi bi-clipboard-fill">'
          }, 1000)
        }
        event.preventDefault()
      })
    })

    // Check if `rawFilesFormWrapper` should be displayed when media type is changed.
    const rawFormRouteSelect = document.getElementById("rawFormRoute")
    const rawFormMediaTypeSelect = document.getElementById("rawFormMediaType")
    const rawFilesFormWrapper = document.getElementById("rawFilesFormWrapper")
    if (rawFilesFormWrapper) {
      const rawFormFilesHandler = () => {
        const selectedRouteOption = rawFormRouteSelect.options[rawFormRouteSelect.selectedIndex]
        if (rawFormMediaTypeSelect.value === "multipart/form-data" && selectedRouteOption.dataset.supportsFiles) {
          rawFilesFormWrapper.style.display = "block"
        } else {
          rawFilesFormWrapper.style.display = "none"
        }
      }

      rawFormRouteSelect.addEventListener("change", rawFormFilesHandler)
      rawFormMediaTypeSelect.addEventListener("change", rawFormFilesHandler)
    }
  })

  // Convert plain-text links to anchor tag links.
  function rrfLinkify(text) {
    return text.replace(/(https?:\/\/[^\s<>"]+)/g, "<a href=\"$1\">$1</a>")
  }

  // Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
  function rrfReplaceDocument(content) {
    // Replace the document with provided content.
    document.open()
    document.write(content)
    document.close()

    // Trigger `DOMContentLoaded` manually so our custom JavaScript works.
    document.dispatchEvent(new Event("DOMContentLoaded", {bubbles: true, cancelable: true}))
  }

  // Refresh the window as a `GET` request.
  function rrfGet(button) {
    button.disabled = true
    window.location.replace(window.location.href)
  }

  // Call `DELETE` on the current path.
  function rrfDelete(button) {
    button.disabled = true
    rrfAPICall(window.location.pathname, "DELETE")
  }

  // Call `OPTIONS` on the current path.
  function rrfOptions(button) {
    button.disabled = true
    rrfAPICall(window.location.pathname, "OPTIONS")
  }

  // Submit the raw form.
  function rrfSubmitRawForm(button) {
    button.disabled = true

    // Grab the selected route/method, media type, and the body.
    const [method, path] = document.getElementById("rawFormRoute").value.split(":")
    const mediaType = document.getElementById("rawFormMediaType").value
    let body = document.getElementById("rawFormContent").value

    // If the media type is `multipart/form-data`, then we need to build a FormData object.
    if (mediaType == "multipart/form-data") {
      let formData = new FormData()

      // Add body to `formData`.
      const bodySearchParams = new URLSearchParams(body)
      bodySearchParams.forEach((value, key) => {
        formData.append(key, value)
      })

      // Add file(s) to `formData`.
      const rawFilesForm = document.getElementById("rawFilesForm")
      if (rawFilesForm) {
        rawFilesForm.querySelectorAll("input[type=file]").forEach((el) => {
          const files = el.files
          for (let i = 0; i < files.length; i++) {
            formData.append(el.name, files[i])
          }
        })
      }

      // Set body to be the form data.
      body = formData
    }

    // Perform the API call.
    rrfAPICall(path, method, {
      body,
      // If the media type is `multipart/form-data`, then we don't want to set the content type
      // because it must be set by `fetch` to include boundary.
      headers: mediaType == "multipart/form-data" ? {} : {"Content-Type": mediaType},
    })
  }

  // Make an HTML API call and replace the document with the response.
  function rrfAPICall(path, method, kwargs={}) {
    const headers = kwargs.headers || {}
    delete kwargs.headers

    fetch(path, {method, headers: {"Accept": "text/html", ...headers}, ...kwargs})
      .then((response) => response.text())
      .then((body) => { rrfReplaceDocument(body) })
  }
</script>