app/views/rest_framework/_head.html.erb
<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("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'")
})
// 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>