Skip to content

Commit

Permalink
refactor: move <script> outside of *.html so they can be linted/fmt
Browse files Browse the repository at this point in the history
  • Loading branch information
lowlighter committed Jun 10, 2024
1 parent 76d7bf8 commit 1ce0fca
Show file tree
Hide file tree
Showing 18 changed files with 255 additions and 289 deletions.
19 changes: 2 additions & 17 deletions app/mod.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<link rel="stylesheet" href="/mod.css">
<link rel="apple-touch-icon" sizes="180x180" href="/180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="/32x32.png">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
</head>
<body class="layout-simple">
<header>
Expand Down Expand Up @@ -351,22 +352,6 @@ <h2 id="unstyled-scripting"><a href="#unstyled-scripting">Scripting</a></h2>
</menu>
</nav>
</footer>
<script>
{
const prefers = matchMedia('(prefers-color-scheme: dark)') ? 'dark' : 'light'
document.querySelectorAll(".example-tabs li.color-scheme").forEach(element => {
element.querySelector(`svg.${prefers}`).style.display = "inline-block"
element.addEventListener("click", event => {
const example = element.parentNode.nextElementSibling
if (example.tagName === "IFRAME") {
example.contentWindow.document.documentElement.dataset.colorScheme = (example.contentWindow.document.documentElement.dataset.colorScheme ?? prefers) === 'light' ? 'dark' : 'light'
}
example.dataset.colorScheme = (example.dataset.colorScheme ?? prefers) === 'light' ? 'dark' : 'light'
element.querySelector("svg.light").style.display = example.dataset.colorScheme === 'light' ? "inline-block" : "none"
element.querySelector("svg.dark").style.display = example.dataset.colorScheme === 'dark' ? "inline-block" : "none"
});
})
}
</script>
<script data-script="/app/mod.js"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions app/mod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference lib="dom" />
const prefers = matchMedia("(prefers-color-scheme: dark)") ? "dark" : "light"
document.querySelectorAll(".example-tabs li.color-scheme").forEach((element) => {
element.querySelector(`svg.${prefers}`).style.display = "inline-block"
element.addEventListener("click", (_) => {
const example = element.parentNode.nextElementSibling
if (example.tagName === "IFRAME") {
example.contentWindow.document.documentElement.dataset.colorScheme = (example.contentWindow.document.documentElement.dataset.colorScheme ?? prefers) === "light" ? "dark" : "light"
}
example.dataset.colorScheme = (example.dataset.colorScheme ?? prefers) === "light" ? "dark" : "light"
element.querySelector("svg.light").style.display = example.dataset.colorScheme === "light" ? "inline-block" : "none"
element.querySelector("svg.dark").style.display = example.dataset.colorScheme === "dark" ? "inline-block" : "none"
})
})
144 changes: 2 additions & 142 deletions app/sections/custom-build.html
Original file line number Diff line number Diff line change
Expand Up @@ -381,22 +381,7 @@ <h3><a href="#custom-build">🛠️ Create a custom build</a></h3>
<textarea rows="8" name="custom">/** Your custom extra CSS here */</textarea>
<div class="highlight"></div>
</div>
<script>
{
const editor = document.currentScript.previousElementSibling
const textarea = editor.querySelector("textarea")
const highlight = editor.querySelector(".highlight")
textarea.addEventListener("input", () => coloration(textarea.value))
textarea.addEventListener("scroll", function () {
highlight.scrollTop = this.scrollTop
highlight.scrollLeft = this.scrollLeft
})
function coloration(value) {
highlight.innerHTML = hljs.highlight(value, { language: "css" }).value
}
coloration(textarea.value)
}
</script>
<script data-script="/styles/@code-editor/mod.js"></script>
</label>
</fieldset>
<fieldset class="miscellaneous">
Expand Down Expand Up @@ -439,130 +424,5 @@ <h3><a href="#custom-build">🛠️ Create a custom build</a></h3>
</fieldset>
<p class="flash danger brewing-error hidden"></p>
</form>
<script>
{
const banner = [
"/**",
` * matcha.css — Custom build (${new Date().toDateString()})`,
` * Copyright © ${new Date().getFullYear()} Lecoq Simon (@lowlighter)`,
" * MIT license — https://github.com/lowlighter/matcha",
" */"
].join("\n")
const form = document.currentScript.previousElementSibling
// Manage meta-checkboxes
form.querySelectorAll("legend input").forEach(input => {
input.addEventListener("change", () => {
form.querySelectorAll(`input[name="${input.dataset.for}"]`).forEach(child => child.checked = input.checked)
})
})
// Manage checkboxes
form.querySelectorAll('input[type="checkbox"]').forEach(input => {
if (input.name) {
input.addEventListener("change", () => {
const checked = Array.from(form.querySelectorAll(`input[name="${input.name}"]`)).reduce((checked, input) => checked && input.checked, true)
form.querySelector(`input[data-for="${input.name}"]`).checked = checked
})
}
})
// Manage variables
function reset(element) {
element.querySelectorAll("input[name]").forEach(input => {
if ((!input.name)||(input.name.endsWith("@opacity")))
return
let value = getComputedStyle(element).getPropertyValue(input.name)
if ((input.type === "color")&&(/^#[a-f0-9]{8}$/i.test(value))) {
const [color, opacity] = [value.slice(0, 7), value.slice(7)]
value = color
element.querySelector(`input[name="${input.name}@opacity"]`).value = opacity
element.querySelector(`input[name="${input.name}@opacity"]`).dataset.default = opacity
}
input.value = value
input.dataset.default = value
})
}
form.querySelectorAll('.variables button[type="reset"]').forEach(button => button.addEventListener("click", event => {
event.preventDefault()
reset(button.closest(".variables"))
}))
form.querySelectorAll(".variables").forEach(group => reset(group))
// Manage preview
const preview = form.querySelector(".preview button")
preview.addEventListener("click", async event => {
event.preventDefault()
const iframe = form.querySelector(".preview iframe")
iframe.src = "/build/demo"
iframe.onload = async function() {
const style = iframe.contentDocument.createElement("style")
style.innerText = await brew()
iframe.contentDocument.head.appendChild(style)
}
})
// Manage brewing
const brewing = form.querySelector(".brewing button")
const teapot = {styling:[], extra:[], variables:[]}
form.addEventListener("change", () => {
const styling = Array.from(form.querySelectorAll('input[name="styling"]'))
const extra = Array.from(form.querySelectorAll('input[name="extra"]'))
const variables = Array.from(form.querySelectorAll('.variables input'))
Object.assign(teapot, {
styling:styling.filter(({checked}) => checked).map(({value}) => value),
extra:extra.filter(({checked}) => checked).map(({value}) => value),
variables:variables.filter(({name, value, dataset}) => (!name.endsWith("@opacity"))&&(value !== dataset.default)).map(({name, value}) => ({name, value}))
})
variables.filter(({name, value, dataset}) => (name.endsWith("@opacity"))&&(value !== dataset.default)).forEach(({name, value:opacity}) => {
const color = form.querySelector(`input[name="${name.replace("@opacity", "")}"]`).value
if (!teapot.variables.find(variable => variable.name === name.replace("@opacity", "")))
teapot.variables.push({name:name.replace("@opacity", ""), value:`${color}${opacity}`})
teapot.variables.find(variable => variable.name === name.replace("@opacity", "")).value = `${color}${opacity}`
})
})
async function brew() {
const custom = form.querySelector('textarea[name="custom"]').value
const root = await fetch("/styles/@root/mod.css").then(response => response.text())
const parts = await Promise.all([...teapot.styling, ...teapot.extra].sort((a, b) => a.localeCompare(b)).map(async name => await fetch(`/styles/${name}/mod.css`).then(response => response.text())))
if (teapot.variables.length) {
const [_, defined, computed] = root.match(/^(?<defined>:root\s*\{[\s\S]*?\})(?<computed>[\s\S*]*)$/)
const stylesheet = new CSSStyleSheet()
stylesheet.insertRule(defined)
for (const {name, value} of teapot.variables) {
stylesheet.rules[0].style.setProperty(name, value)
}
parts.unshift(stylesheet.rules[0].cssText, computed)
}
else {
parts.unshift(root)
}
return [banner, ...parts, custom.trim()].join("\n")
}
// Manage brewed download
form.querySelector(".brewing button").addEventListener("click", async event => {
event.preventDefault()
const minify = form.querySelector('input[name="minify"]').checked
const stylesheet = await brew()
try {
brewing.toggleAttribute("disabled", true)
brewing.style.cursor = "loading"
let url = URL.createObjectURL(new Blob([new TextEncoder().encode(stylesheet)], {type:"text/css;charset=utf-8"}))
if (minify) {
const response = await fetch("/api/brew", {method:"POST", body:stylesheet})
if (response.status !== 200) {
throw new Error(await response.text())
}
url = URL.createObjectURL(new Blob([await response.blob()], {type:"text/css;charset=utf-8"}))
}
globalThis.open(url, "_blank")
}
catch (error) {
document.querySelector(".brewing-error").classList.remove("hidden")
document.querySelector(".brewing-error").innerText = error.message
setTimeout(() => document.querySelector(".brewing-error").classList.add("hidden"), 15*1000)
}
finally {
brewing.toggleAttribute("disabled", false)
brewing.style.removeProperty("cursor")
}
})
form.dispatchEvent(new Event("change"))
}
</script>
<script data-script="/app/sections/custom-build.js"></script>
</details>
126 changes: 126 additions & 0 deletions app/sections/custom-build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/// <reference lib="dom" />
const banner = [
"/**",
` * matcha.css — Custom build (${new Date().toDateString()})`,
` * Copyright © ${new Date().getFullYear()} Lecoq Simon (@lowlighter)`,
" * MIT license — https://github.com/lowlighter/matcha",
" */",
].join("\n")
const form = document.currentScript.previousElementSibling
// Manage meta-checkboxes
form.querySelectorAll("legend input").forEach((input) => {
input.addEventListener("change", () => {
form.querySelectorAll(`input[name="${input.dataset.for}"]`).forEach((child) => child.checked = input.checked)
})
})
// Manage checkboxes
form.querySelectorAll('input[type="checkbox"]').forEach((input) => {
if (input.name) {
input.addEventListener("change", () => {
const checked = Array.from(form.querySelectorAll(`input[name="${input.name}"]`)).reduce((checked, input) => checked && input.checked, true)
form.querySelector(`input[data-for="${input.name}"]`).checked = checked
})
}
})
// Manage variables
function reset(element) {
element.querySelectorAll("input[name]").forEach((input) => {
if ((!input.name) || (input.name.endsWith("@opacity"))) {
return
}
let value = getComputedStyle(element).getPropertyValue(input.name)
if ((input.type === "color") && (/^#[a-f0-9]{8}$/i.test(value))) {
const [color, opacity] = [value.slice(0, 7), value.slice(7)]
value = color
element.querySelector(`input[name="${input.name}@opacity"]`).value = opacity
element.querySelector(`input[name="${input.name}@opacity"]`).dataset.default = opacity
}
input.value = value
input.dataset.default = value
})
}
form.querySelectorAll('.variables button[type="reset"]').forEach((button) =>
button.addEventListener("click", (event) => {
event.preventDefault()
reset(button.closest(".variables"))
})
)
form.querySelectorAll(".variables").forEach((group) => reset(group))
// Manage preview
const preview = form.querySelector(".preview button")
preview.addEventListener("click", (event) => {
event.preventDefault()
const iframe = form.querySelector(".preview iframe")
iframe.src = "/build/demo"
iframe.onload = async function () {
const style = iframe.contentDocument.createElement("style")
style.innerText = await brew()
iframe.contentDocument.head.appendChild(style)
}
})
// Manage brewing
const brewing = form.querySelector(".brewing button")
const teapot = { styling: [], extra: [], variables: [] }
form.addEventListener("change", () => {
const styling = Array.from(form.querySelectorAll('input[name="styling"]'))
const extra = Array.from(form.querySelectorAll('input[name="extra"]'))
const variables = Array.from(form.querySelectorAll(".variables input"))
Object.assign(teapot, {
styling: styling.filter(({ checked }) => checked).map(({ value }) => value),
extra: extra.filter(({ checked }) => checked).map(({ value }) => value),
variables: variables.filter(({ name, value, dataset }) => (!name.endsWith("@opacity")) && (value !== dataset.default)).map(({ name, value }) => ({ name, value })),
})
variables.filter(({ name, value, dataset }) => (name.endsWith("@opacity")) && (value !== dataset.default)).forEach(({ name, value: opacity }) => {
const color = form.querySelector(`input[name="${name.replace("@opacity", "")}"]`).value
if (!teapot.variables.find((variable) => variable.name === name.replace("@opacity", ""))) {
teapot.variables.push({ name: name.replace("@opacity", ""), value: `${color}${opacity}` })
}
teapot.variables.find((variable) => variable.name === name.replace("@opacity", "")).value = `${color}${opacity}`
})
})
async function brew() {
const custom = form.querySelector('textarea[name="custom"]').value
const root = await fetch("/styles/@root/mod.css").then((response) => response.text())
const parts = await Promise.all(
[...teapot.styling, ...teapot.extra].sort((a, b) => a.localeCompare(b)).map(async (name) => await fetch(`/styles/${name}/mod.css`).then((response) => response.text())),
)
if (teapot.variables.length) {
const [_, defined, computed] = root.match(/^(?<defined>:root\s*\{[\s\S]*?\})(?<computed>[\s\S*]*)$/)
const stylesheet = new CSSStyleSheet()
stylesheet.insertRule(defined)
for (const { name, value } of teapot.variables) {
stylesheet.rules[0].style.setProperty(name, value)
}
parts.unshift(stylesheet.rules[0].cssText, computed)
} else {
parts.unshift(root)
}
return [banner, ...parts, custom.trim()].join("\n")
}
// Manage brewed download
form.querySelector(".brewing button").addEventListener("click", async (event) => {
event.preventDefault()
const minify = form.querySelector('input[name="minify"]').checked
const stylesheet = await brew()
try {
brewing.toggleAttribute("disabled", true)
brewing.style.cursor = "loading"
let url = URL.createObjectURL(new Blob([new TextEncoder().encode(stylesheet)], { type: "text/css;charset=utf-8" }))
if (minify) {
const response = await fetch("/api/brew", { method: "POST", body: stylesheet })
if (response.status !== 200) {
throw new Error(await response.text())
}
url = URL.createObjectURL(new Blob([await response.blob()], { type: "text/css;charset=utf-8" }))
}
globalThis.open(url, "_blank")
} catch (error) {
document.querySelector(".brewing-error").classList.remove("hidden")
document.querySelector(".brewing-error").innerText = error.message
setTimeout(() => document.querySelector(".brewing-error").classList.add("hidden"), 15 * 1000)
} finally {
brewing.toggleAttribute("disabled", false)
brewing.style.removeProperty("cursor")
}
})
form.dispatchEvent(new Event("change"))
19 changes: 1 addition & 18 deletions app/sections/preview-website.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,5 @@ <h3><a href="#preview-website">🕵️ Preview a Website</a></h3>
</li>
</menu>
<iframe class="matcha-preview" loading="lazy" title="preview" height="420" class="bd-muted"></iframe>
<script>
{
const form = document.currentScript.parentElement.querySelector("form")
const iframe = document.currentScript.parentElement.querySelector("iframe")
form.addEventListener("submit", async event => {
event.preventDefault()
iframe.src = `/api/preview?url=${form.querySelector("input").value}`
iframe.onload = function() {
const origin = new URL(new URL(iframe.src).searchParams.get("url")).href
iframe.contentDocument.querySelectorAll("style,script").forEach(element => element.remove())
const link = iframe.contentDocument.createElement("link")
link.href = "/matcha.css"
link.rel = "stylesheet"
iframe.contentDocument.head.appendChild(link)
}
})
}
</script>
<script data-script="/app/sections/preview-website.js"></script>
</details>
14 changes: 14 additions & 0 deletions app/sections/preview-website.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference lib="dom" />
const form = document.currentScript.parentElement.querySelector("form")
const iframe = document.currentScript.parentElement.querySelector("iframe")
form.addEventListener("submit", (event) => {
event.preventDefault()
iframe.src = `/api/preview?url=${form.querySelector("input").value}`
iframe.onload = function () {
iframe.contentDocument.querySelectorAll("style,script").forEach((element) => element.remove())
const link = iframe.contentDocument.createElement("link")
link.href = "/matcha.css"
link.rel = "stylesheet"
iframe.contentDocument.head.appendChild(link)
}
})
Loading

0 comments on commit 1ce0fca

Please sign in to comment.