Compare commits

..
8 changed files with 347 additions and 169 deletions

View file

@ -168,7 +168,7 @@ func render(b *strings.Builder, nodes []Node) {
b.WriteString("</figure>")
case *Audio:
b.WriteString(`<audio controls="" preload="metadata" src="/uploaded/`)
b.WriteString(`<audio controls="" preload="metadata" src="`)
b.WriteString(html.EscapeString(safeUrl(v.URL)))
b.WriteString(`" type="audio/mpeg"></audio>`)
case *Aside:

View file

@ -326,72 +326,99 @@
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
align-items: start;
gap: 10px;
position: relative;
form {
max-width: unset;
flex-basis: 50%;
width: 100%;
}
article {
flex-basis: 50%;
width: 100%;
}
textarea {
max-width: unset;
height: 80vh;
}
}
.media-picker {
padding-left:0;
overflow-y: scroll;
max-height: 50vh;
> *+* {
margin-top: 1em;
width: 100%;
}
li {
padding:0;
}
.media-preview-container {
position: sticky;
top: var(--margin-s);
}
.toggle-preview-btn {
position: static;
margin: var(--margin-s) 0;
}
.media-picker {
display: none;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
overflow-y: scroll;
height: 100%;
max-height: 60vh;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.media-picker__media {
display: grid;
grid-template-columns: 150px 1fr;
gap:1em;
line-height: 1;
aspect-ratio: 1/1;
position: relative;
img,
audio,
video {
width: 150px;
height: auto;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.media-picker__buttons {
display: flex;
margin-top:1em;
gap: 0.5em;
align-items: flex-start;
visibility: hidden;
display: flex;
flex-direction: column;
position: absolute;
align-items: center;
justify-content: center;
inset: 0;
gap: 6px;
button {
flex: 0 1 32px;
}
}
[theme="raw"] .media-picker__buttons {
display: none;
.media-picker__media:hover .media-picker__buttons {
visibility: visible;
}
.page-preview {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: var(--padding-form);
overflow-y: scroll;
max-height: 100vh;
display: block;
svg {
max-width: 100%;
}
}
.media-preview-container[data-show="false"] {
display: none;
}
.media-preview-container[data-show="gallery"] {
.media-picker {
display: grid;
}
.page-preview {
display: none;
}
}
.media-preview-container[data-show="preview"] {
.media-picker {
display: none;
}
.page-preview {
display: block;
}
}
.edit-footer {
position: fixed;

View file

@ -2,13 +2,17 @@
<main>
<h1>New page</h1>
<div class="edit-footer">
<div>
<button data-small data-secondary onClick="toggleGallery()" id="mediasBtn">Medias</button>
<button data-small onClick="togglePreview()" id="previewBtn">Preview</button>
</div>
<div>
<form data-naked action='/files/upload' method='POST' class="media-form">
<div>
<input type="file" multiple="multiple" id="fileUpload" value="Upload file" />
</div>
<div>
<button data-small type='submit' data-size="20" id="uploadMediaBtn" >
<button data-small type='submit' data-size="20" id="uploadMediaBtn" disabled>
Upload file
</button>
</div>
@ -19,15 +23,6 @@
<div class="edit-page">
<form action='/page/create' method='POST'>
<input type='hidden' id="csrf_token" name='csrf_token' value='{{.CSRFToken}}'>
<label for="bodyError">Content:</label>
<textarea id="body" name='body' {{with .Form.FieldErrors.body}} aria-invalid aria-describedby="bodyError" {{end}}>{{.Form.Body}}</textarea>
{{with .Form.FieldErrors.body}}
<span id="summaryError">{{.}}</span>
{{end}}
{{template "mediaselector" .}}
<label for="name">Title:</label>
<input autofocus type='text' id="name" name='name' value="{{.Form.Name}}" {{with .Form.FieldErrors.name}} aria-invalid
aria-describedby="nameError" {{end}}>
@ -42,6 +37,11 @@
<span id="summaryError">{{.}}</span>
{{end}}
<label for="bodyError">Content:</label>
<textarea id="body" name='body' {{with .Form.FieldErrors.body}} aria-invalid aria-describedby="bodyError" {{end}}>{{.Form.Body}}</textarea>
{{with .Form.FieldErrors.body}}
<span id="summaryError">{{.}}</span>
{{end}}
<div class="grouped">
<div>
@ -111,11 +111,50 @@
<input type='submit' value='Publish page' data-size="20">
</div>
</form>
<div class="media-preview-container" data-show="preview" >
<div class="media-picker">
{{range $image := .Files}}
{{if eq $image.Extension ".png" ".jpg" ".jpeg" ".gif" ".webp" ".avif"}}
<div class="media-picker__media" id="{{$image.Filename}}">
<img loading="lazy" src="{{$image.Slug}}" />
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true)">Insert Image</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}})">Delete</button>
</div>
</div>
{{end}}
{{if eq $image.Extension ".mp4"}}
<div class="media-picker__media">
<video src="{{$image.Slug}}" ></video>
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true)">Insert Video</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}})">Delete</button>
</div>
</div>
{{end}}
{{if eq $image.Extension ".mp3"}}
<div class="media-picker__media">
<div style="text-align: center;">
<strong>{{$image.Filename}}{{$image.Extension}}</strong>
</div>
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true)">Insert Aduio</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}})">Delete</button>
</div>
</div>
{{end}}
{{end}}
</div>
<article class="page-preview {{.Form.Category}}"></article>
</div>
</div>
</main>
<script>
autosize(document.querySelector('textarea'));
const formated = document.querySelector('article');
const textarea = document.querySelector('textarea');

View file

@ -2,6 +2,10 @@
<main>
<h1>Edit page</h1>
<div class="edit-footer">
<div>
<button data-small data-secondary onClick="toggleGallery()" id="mediasBtn">Medias</button>
<button data-small onClick="togglePreview()" id="previewBtn">Preview</button>
</div>
<div>
<form data-naked action='/files/upload' method='POST' class="media-form">
<div>
@ -22,17 +26,11 @@
<input type="hidden" value="{{.Form.Id}}" name="id"/>
<input type="hidden" value="{{.Form.Slug}}" name="slug"/>
<label for="bodyError">Content:</label>
<textarea autofocus id="body" name='body' {{with .Form.FieldErrors.body}} aria-invalid aria-describedby="bodyError" {{end}}>{{.Form.Body}}</textarea>
{{with .Form.FieldErrors.body}}
<span id="summaryError">{{.}}</span>
{{end}}
{{template "mediaselector" .}}
<img src="/og/{{.Form.Slug}}" />
<label for="name">Title:</label>
<span>Changing the title will also change the page url</span>
<input type='text' id="name" name='name' value="{{.Form.Name}}" {{with .Form.FieldErrors.name}} aria-invalid
<input autofocus type='text' id="name" name='name' value="{{.Form.Name}}" {{with .Form.FieldErrors.name}} aria-invalid
aria-describedby="nameError" {{end}}>
{{with .Form.FieldErrors.name}}
<span id="nameError">{{.}}</span>
@ -45,6 +43,14 @@
<span id="summaryError">{{.}}</span>
{{end}}
<label for="bodyError">Content:</label>
<textarea id="body" name='body' {{with .Form.FieldErrors.body}} aria-invalid aria-describedby="bodyError" {{end}}>{{.Form.Body}}</textarea>
{{with .Form.FieldErrors.body}}
<span id="summaryError">{{.}}</span>
{{end}}
<div class="grouped">
<div>
{{ $host := .Form.Host }}
@ -108,21 +114,54 @@
<label for="menu">Show Sub Pages List</label>
</fieldset>
<div>
<details>
<summary>OG Image Preview</summary>
<img src="/og/{{.Form.Slug}}" />
</div>
<div>
<button type='submit' data-size="20">Save changes</button>
</div>
</form>
<div class="media-preview-container" data-show="preview" >
<div class="media-picker">
{{range $image := .Files}}
{{if eq $image.Extension ".png" ".jpg" ".jpeg" ".gif" ".webp" ".avif"}}
<div class="media-picker__media" id="{{$image.Filename}}">
<img loading="lazy" src="{{$image.Slug}}" />
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true)">Insert Image</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}})">Delete</button>
</div>
</div>
{{end}}
{{if eq $image.Extension ".mp4"}}
<div class="media-picker__media">
<video src="{{$image.Slug}}" ></video>
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true)">Insert Video</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}})">Delete</button>
</div>
</div>
{{end}}
{{if eq $image.Extension ".mp3"}}
<div class="media-picker__media">
<div style="text-align: center;">
<strong>{{$image.Filename}}{{$image.Extension}}</strong>
</div>
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true)">Insert Audio</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}})">Delete</button>
</div>
</div>
{{end}}
{{end}}
</div>
<article class="page-preview {{.Form.Category}}"></article>
</div>
</div>
</main>
<script>
autosize(document.querySelector('textarea'));
const formated = document.querySelector('article');
const textarea = document.querySelector('textarea');

View file

@ -1,54 +0,0 @@
{{define "mediaselector"}}
<div>
<details>
<summary>Medias</summary>
<ul role="list" class="media-picker">
{{range $image := .Files}}
{{if eq $image.Extension ".png" ".jpg" ".jpeg" ".gif" ".webp" ".avif"}}
<li class="media-picker__media" id="{{$image.Filename}}">
<img loading="lazy" src="{{$image.Slug}}" />
<span>
<small><strong>{{$image.Filename}}{{$image.Extension}}</strong></small><br>
<small><code>(image: {{$image.Filename}}{{$image.Extension}} alt: )</code></small>
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true, event)">Insert</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}}, event)">Delete</button>
</div>
</span>
</li>
{{end}}
{{if eq $image.Extension ".mp4"}}
<li class="media-picker__media" id="{{$image.Filename}}">
<video controls src="{{$image.Slug}}" ></video>
<span>
<small><strong>{{$image.Filename}}{{$image.Extension}}</strong></small><br>
<small><code>(video: {{$image.Filename}}{{$image.Extension}})</code></small>
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true, event)">Insert Video</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}}, event)">Delete</button>
</div>
</span>
</li>
{{end}}
{{if eq $image.Extension ".mp3"}}
<li class="media-picker__media" id="{{$image.Filename}}">
<audio controls src="{{$image.Slug}}"></audio>
<span>
<small><strong>{{$image.Filename}}{{$image.Extension}}</strong></small><br>
<small><code>(audio: {{$image.Filename}}{{$image.Extension}})</code></small>
<div class="media-picker__buttons">
<button data-small onClick="insertMedia({{$image.Filename}}+{{$image.Extension}}, {{$image.Extension}}, true, event)">Insert audio</button>
<button data-small data-secondary
onClick="deleteMedia({{$image.Filename}}+{{$image.Extension}}, event)">Delete</button>
</div>
</span>
</li>
{{end}}
{{end}}
</ul>
</details>
</div>
{{end}}

View file

@ -326,16 +326,58 @@ background: lightgrey;
}
textarea {
max-width: unset;
width:100%;
height:30vh;
}
}
.media-preview-container {
position: sticky;
top: 20px;
}
.toggle-preview-btn {
position: static;
margin: 20px 0;
}
.media-picker {
display: none;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
overflow-y: scroll;
height: 100%;
max-height: 60vh;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.media-picker__media {
aspect-ratio: 1/1;
position: relative;
img,
video {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.media-picker__buttons {
visibility: hidden;
display: flex;
flex-direction: column;
position: absolute;
align-items: center;
justify-content: center;
inset: 0;
gap: 6px;
button {
flex: 0 1 32px;
}
}
.media-picker__media:hover .media-picker__buttons {
visibility: visible;
}
.page-preview {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
@ -347,56 +389,40 @@ background: lightgrey;
}
}
.media-picker {
padding-left:0;
overflow-y: scroll;
max-height: 50vh;
> *+* {
margin-top: 1em;
width: 100%;
}
li {
padding:0;
}
.media-preview-container[data-show="false"] {
display: none;
}
.media-picker__media {
display: grid;
grid-template-columns: 150px 1fr;
gap:1em;
line-height: 1;
img,
audio,
video {
width: 150px;
height: auto;
.media-preview-container[data-show="gallery"] {
.media-picker {
display: grid;
}
.page-preview {
display: none;
}
}
.media-picker__buttons {
display: flex;
margin-top:1em;
gap: 0.5em;
align-items: flex-start;
}
[theme="raw"] .media-picker__buttons {
.media-preview-container[data-show="preview"] {
.media-picker {
display: none;
}
.page-preview {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: var(--padding-form);
overflow-y: scroll;
max-height: 100vh;
svg {
max-width: 100%;
}
.page-preview {
display: block;
}
}
.edit-footer {
position: fixed;
width: 100%;
background-color: var(--background);
bottom: 0;
left: 0;
z-index: 1;
padding: calc(20px / 2) 40px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: calc(20px / 4) calc(20px);
}
@media (max-width: 960px) {
@ -417,16 +443,5 @@ max-height: 100vh;
}
form > * {
display: block;
}
form > * + *, .grouped > *+*{
margin-top: var(--margin-s);
margin-top: 10px;
width:100%;
}
</style>
{{end}}

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,6 @@
// Focus-Aware Keystroke Pattern Detector with Redirects
// This script detects keyboard shortcuts only when NOT focusing on input elements
document.addEventListener('DOMContentLoaded', function () {
// Variables to store keystroke history
let keystrokeHistory = '';
@ -69,6 +70,117 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
// Autosize
!function (e, t) {
"object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e || self).autosize = t()
}(this, function () {
var e = new Map;
function t(t) {
var o = e.get(t);
o && o.destroy()
}
function o(t) {
var o = e.get(t);
o && o.update()
}
var r = null;
return "undefined" == typeof window ? ((r = function (e) {
return e
}).destroy = function (e) {
return e
}, r.update = function (e) {
return e
}) : ((r = function (t, o) {
return t && Array.prototype.forEach.call(t.length ? t : [t], function (t) {
return function (t) {
if (t && t.nodeName && "TEXTAREA" === t.nodeName && !e.has(t)) {
var o, r = null, n = window.getComputedStyle(t), i = (o = t.value, function () {
s({
testForHeightReduction: "" === o || !t.value.startsWith(o),
restoreTextAlign: null
}), o = t.value
}), l = function (o) {
t.removeEventListener("autosize:destroy", l), t.removeEventListener("autosize:update", a), t.removeEventListener("input", i), window.removeEventListener("resize", a), Object.keys(o).forEach(function (e) {
return t.style[e] = o[e]
}), e.delete(t)
}.bind(t, {
height: t.style.height,
resize: t.style.resize,
textAlign: t.style.textAlign,
overflowY: t.style.overflowY,
overflowX: t.style.overflowX,
wordWrap: t.style.wordWrap
});
t.addEventListener("autosize:destroy", l), t.addEventListener("autosize:update", a), t.addEventListener("input", i), window.addEventListener("resize", a), t.style.overflowX = "hidden", t.style.wordWrap = "break-word", e.set(t, {
destroy: l,
update: a
}), a()
}
function s(e) {
var o, i, l = e.restoreTextAlign, a = void 0 === l ? null : l, d = e.testForHeightReduction,
u = void 0 === d || d, f = n.overflowY;
if (0 !== t.scrollHeight && ("vertical" === n.resize ? t.style.resize = "none" : "both" === n.resize && (t.style.resize = "horizontal"), u && (o = function (e) {
for (var t = []; e && e.parentNode && e.parentNode instanceof Element;) e.parentNode.scrollTop && t.push([e.parentNode, e.parentNode.scrollTop]), e = e.parentNode;
return function () {
return t.forEach(function (e) {
var t = e[0], o = e[1];
t.style.scrollBehavior = "auto", t.scrollTop = o, t.style.scrollBehavior = null
})
}
}(t), t.style.height = ""), i = "content-box" === n.boxSizing ? t.scrollHeight - (parseFloat(n.paddingTop) + parseFloat(n.paddingBottom)) : t.scrollHeight + parseFloat(n.borderTopWidth) + parseFloat(n.borderBottomWidth), "none" !== n.maxHeight && i > parseFloat(n.maxHeight) ? ("hidden" === n.overflowY && (t.style.overflow = "scroll"), i = parseFloat(n.maxHeight)) : "hidden" !== n.overflowY && (t.style.overflow = "hidden"), t.style.height = i + "px", a && (t.style.textAlign = a), o && o(), r !== i && (t.dispatchEvent(new Event("autosize:resized", { bubbles: !0 })), r = i), f !== n.overflow && !a)) {
var c = n.textAlign;
"hidden" === n.overflow && (t.style.textAlign = "start" === c ? "end" : "start"), s({
restoreTextAlign: c,
testForHeightReduction: !0
})
}
}
function a() {
s({ testForHeightReduction: !0, restoreTextAlign: null })
}
}(t)
}), t
}).destroy = function (e) {
return e && Array.prototype.forEach.call(e.length ? e : [e], t), e
}, r.update = function (e) {
return e && Array.prototype.forEach.call(e.length ? e : [e], o), e
}), r
});
function toggleGallery(e) {
const mediasBtn = document.querySelector("#mediasBtn")
const previewBtn = document.querySelector("#previewBtn")
const mediaPreviewContainer = document.querySelector(".media-preview-container")
if (mediaPreviewContainer.dataset.show !== "gallery") {
mediaPreviewContainer.dataset.show = "gallery"
delete (mediasBtn.dataset.secondary)
previewBtn.setAttribute("data-secondary", "");
} else {
mediaPreviewContainer.dataset.show = false
mediasBtn.setAttribute("data-secondary", "");
}
}
function togglePreview(e) {
const previewBtn = document.querySelector("#previewBtn")
const mediasBtn = document.querySelector("#mediasBtn")
const mediaPreviewContainer = document.querySelector(".media-preview-container")
if (mediaPreviewContainer.dataset.show !== "preview") {
mediaPreviewContainer.dataset.show = "preview"
delete (previewBtn.dataset.secondary)
mediasBtn.setAttribute("data-secondary", "");
} else {
mediaPreviewContainer.dataset.show = false
previewBtn.setAttribute("data-secondary", "");
}
}
let debounceTimer;
function triggerDemo() {
clearTimeout(debounceTimer);
@ -83,8 +195,8 @@ function triggerDemo() {
}
// Insert text at saved cursor position (works even when not focused)
function insertMedia(slug, extension, reposition, event) {
event.preventDefault()
function insertMedia(slug, extension, reposition) {
console.log({ reposition })
const value = textarea.value;
let textInsert = ""
@ -110,15 +222,15 @@ function insertMedia(slug, extension, reposition, event) {
textarea.setSelectionRange(savedCursor, savedCursor);
}
// togglePreview()
togglePreview()
setTimeout(() => {
triggerDemo()
}, 400)
}, 100)
}
function deleteMedia(filename, event) {
event.preventDefault()
function deleteMedia(filename) {
if (!confirm(`Delete "${filename}"?`)) return;
const csrfToken = document.querySelector('#csrf_token')?.value;
fetch(`/files/delete/${encodeURIComponent(filename)}`, {
@ -130,7 +242,7 @@ function deleteMedia(filename, event) {
const responseText = await response.text();
if (response.ok) {
const mediaItem = document.querySelector(`#${filename.split('.').slice(0, -1)[0]}`)
const mediaItem = document.querySelector(`#${filename.split('.').slice(0, -1)}`)
if (mediaItem) {
mediaItem.remove()
}