This commit is contained in:
@@ -458,6 +458,8 @@ func TestQuestionPageRendersNestedPostControls(t *testing.T) {
|
|||||||
"Shop response",
|
"Shop response",
|
||||||
"Edited",
|
"Edited",
|
||||||
`action="/posts"`,
|
`action="/posts"`,
|
||||||
|
`data-submit-once`,
|
||||||
|
`data-submit-button`,
|
||||||
`action="/posts/` + root.ID + `/edit"`,
|
`action="/posts/` + root.ID + `/edit"`,
|
||||||
`action="/posts/` + homeownerReply.ID + `/edit"`,
|
`action="/posts/` + homeownerReply.ID + `/edit"`,
|
||||||
`>The model number is 123A.</textarea>`,
|
`>The model number is 123A.</textarea>`,
|
||||||
|
|||||||
@@ -168,6 +168,16 @@ func TestRegisterLoginAsk(t *testing.T) {
|
|||||||
if rec.Code != 200 {
|
if rec.Code != 200 {
|
||||||
t.Fatalf("submit form %d", rec.Code)
|
t.Fatalf("submit form %d", rec.Code)
|
||||||
}
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
`src="/static/app.js"`,
|
||||||
|
`id="submit-progress"`,
|
||||||
|
`data-submit-once`,
|
||||||
|
`data-submit-button`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(rec.Body.String(), want) {
|
||||||
|
t.Fatalf("submit form missing %q: %s", want, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
csrf := csrfFrom(rec.Body.String())
|
csrf := csrfFrom(rec.Body.String())
|
||||||
form := strings.NewReader("_csrf=" + csrf + "&title=Leaky+faucet&body=Drip+all+night.&city=Oakland")
|
form := strings.NewReader("_csrf=" + csrf + "&title=Leaky+faucet&body=Drip+all+night.&city=Oakland")
|
||||||
req = httptest.NewRequest(http.MethodPost, "/submit", form)
|
req = httptest.NewRequest(http.MethodPost, "/submit", form)
|
||||||
|
|||||||
@@ -41,6 +41,31 @@ body {
|
|||||||
background-size: 100% 100%, 48px 48px;
|
background-size: 100% 100%, 48px 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submit-progress {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
z-index: 100;
|
||||||
|
height: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-progress[hidden] { display: none; }
|
||||||
|
|
||||||
|
.submit-progress-bar {
|
||||||
|
display: block;
|
||||||
|
width: 35%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--signal);
|
||||||
|
box-shadow: 0 0 12px var(--signal);
|
||||||
|
animation: submit-progress 900ms ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes submit-progress {
|
||||||
|
from { transform: translateX(-100%); }
|
||||||
|
to { transform: translateX(300%); }
|
||||||
|
}
|
||||||
|
|
||||||
img, svg { display: block; }
|
img, svg { display: block; }
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -253,6 +278,12 @@ a {
|
|||||||
|
|
||||||
.btn-primary:hover { filter: brightness(1.08); }
|
.btn-primary:hover { filter: brightness(1.08); }
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.65;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-ghost {
|
.btn-ghost {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
@@ -763,6 +794,10 @@ input:focus, textarea:focus, .btn:focus-visible, .chip:focus-visible, .vote-btn:
|
|||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.btn-primary:hover { filter: none; }
|
.btn-primary:hover { filter: none; }
|
||||||
|
.submit-progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-users { margin-top: 20px; overflow-x: auto; }
|
.admin-users { margin-top: 20px; overflow-x: auto; }
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
(() => {
|
||||||
|
const formSelector = "form[data-submit-once]";
|
||||||
|
|
||||||
|
function progressIndicator() {
|
||||||
|
return document.getElementById("submit-progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm(form) {
|
||||||
|
form.removeAttribute("aria-busy");
|
||||||
|
delete form.dataset.submitting;
|
||||||
|
|
||||||
|
const button = form.querySelector("[data-submit-button]");
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.disabled = false;
|
||||||
|
button.removeAttribute("aria-disabled");
|
||||||
|
if (button.dataset.idleLabel) {
|
||||||
|
button.textContent = button.dataset.idleLabel;
|
||||||
|
delete button.dataset.idleLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("submit", (event) => {
|
||||||
|
const form = event.target.closest(formSelector);
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (form.dataset.submitting === "true") {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.dataset.submitting = "true";
|
||||||
|
form.setAttribute("aria-busy", "true");
|
||||||
|
|
||||||
|
const button = event.submitter || form.querySelector("[data-submit-button]");
|
||||||
|
if (button) {
|
||||||
|
button.dataset.idleLabel = button.textContent;
|
||||||
|
button.textContent = form.dataset.submittingLabel || "Posting…";
|
||||||
|
button.disabled = true;
|
||||||
|
button.setAttribute("aria-disabled", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = progressIndicator();
|
||||||
|
if (progress) {
|
||||||
|
progress.hidden = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("pageshow", () => {
|
||||||
|
document.querySelectorAll(formSelector).forEach(resetForm);
|
||||||
|
const progress = progressIndicator();
|
||||||
|
if (progress) {
|
||||||
|
progress.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -11,8 +11,13 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/app.css">
|
<link rel="stylesheet" href="/static/app.css">
|
||||||
<script src="/static/htmx.min.js" defer></script>
|
<script src="/static/htmx.min.js" defer></script>
|
||||||
|
<script src="/static/app.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="submit-progress" class="submit-progress" role="progressbar"
|
||||||
|
aria-label="Posting" aria-valuetext="Posting" hidden>
|
||||||
|
<span class="submit-progress-bar"></span>
|
||||||
|
</div>
|
||||||
<a class="skip" href="#main">Skip to content</a>
|
<a class="skip" href="#main">Skip to content</a>
|
||||||
<header class="top">
|
<header class="top">
|
||||||
<div class="top-inner">
|
<div class="top-inner">
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
{{if canReply .User .Root}}
|
{{if canReply .User .Root}}
|
||||||
<details class="post-composer">
|
<details class="post-composer">
|
||||||
<summary>Reply</summary>
|
<summary>Reply</summary>
|
||||||
<form class="post-form" method="post" action="/posts">
|
<form class="post-form" method="post" action="/posts"
|
||||||
|
data-submit-once data-submitting-label="Posting…">
|
||||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||||
<input type="hidden" name="parent_id" value="{{.Post.ID}}">
|
<input type="hidden" name="parent_id" value="{{.Post.ID}}">
|
||||||
<label for="reply-{{.Post.ID}}">Reply to {{.Post.AuthorName}}</label>
|
<label for="reply-{{.Post.ID}}">Reply to {{.Post.AuthorName}}</label>
|
||||||
<textarea id="reply-{{.Post.ID}}" name="body" rows="5" required maxlength="12000"></textarea>
|
<textarea id="reply-{{.Post.ID}}" name="body" rows="5" required maxlength="12000"></textarea>
|
||||||
<div class="post-form-actions">
|
<div class="post-form-actions">
|
||||||
<button type="submit" class="btn btn-primary">Post reply</button>
|
<button type="submit" class="btn btn-primary" data-submit-button>Post reply</button>
|
||||||
<button type="reset" class="btn btn-ghost"
|
<button type="reset" class="btn btn-ghost"
|
||||||
onclick="this.closest('details').removeAttribute('open')">Cancel</button>
|
onclick="this.closest('details').removeAttribute('open')">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<h1>Ask a question</h1>
|
<h1>Ask a question</h1>
|
||||||
<p class="lede">It lands on today’s hunt (Pacific time). People vote; the ranking resets at midnight PT.</p>
|
<p class="lede">It lands on today’s hunt (Pacific time). People vote; the ranking resets at midnight PT.</p>
|
||||||
{{if .Error}}<p class="banner error" role="alert">{{.Error}}</p>{{end}}
|
{{if .Error}}<p class="banner error" role="alert">{{.Error}}</p>{{end}}
|
||||||
<form class="ask" method="post" action="/submit">
|
<form class="ask" method="post" action="/submit"
|
||||||
|
data-submit-once data-submitting-label="Posting…">
|
||||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||||
<label for="title">Title</label>
|
<label for="title">Title</label>
|
||||||
<input id="title" name="title" type="text" required maxlength="120" value="{{.TitleVal}}" placeholder="Water heater popping after showers">
|
<input id="title" name="title" type="text" required maxlength="120" value="{{.TitleVal}}" placeholder="Water heater popping after showers">
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
<textarea id="body" name="body" rows="8" required maxlength="8000" placeholder="Age of the house, what you already tried, where you are in the Bay if it helps.">{{.BodyVal}}</textarea>
|
<textarea id="body" name="body" rows="8" required maxlength="8000" placeholder="Age of the house, what you already tried, where you are in the Bay if it helps.">{{.BodyVal}}</textarea>
|
||||||
<label for="city">City <span class="optional">(optional)</span></label>
|
<label for="city">City <span class="optional">(optional)</span></label>
|
||||||
<input id="city" name="city" type="text" maxlength="80" value="{{.CityVal}}" placeholder="Oakland">
|
<input id="city" name="city" type="text" maxlength="80" value="{{.CityVal}}" placeholder="Oakland">
|
||||||
<button type="submit" class="btn btn-primary">Submit to today’s hunt</button>
|
<button type="submit" class="btn btn-primary" data-submit-button>Submit to today’s hunt</button>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
|
|||||||
Reference in New Issue
Block a user