vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Show a spinner while a form post is in flight
3a068983ae40271962428eff5e2568446b36299d · Keenan Nemetz · 11d ago · parent 628d4523
server/web/static/app.js +70
| @@ -52,6 +52,76 @@ const copyText = (value, done) => { | |||
| 52 | 52 | }); | |
| 53 | 53 | })(); | |
| 54 | 54 | ||
| 55 | // A form post is a server round trip that can take a while: landing a change | ||
| 56 | // request writes the merge, a repository rename moves the store. The submit | ||
| 57 | // button doubles as the progress indicator. It is disabled before the request | ||
| 58 | // leaves, so a second click cannot run the same mutation twice, and after a short | ||
| 59 | // delay its icon gives way to a spinner. The delay keeps a fast save from | ||
| 60 | // flashing a loader. A successful write answers with a redirect or a new page, | ||
| 61 | // so the next page is the reset; pageshow puts the buttons back when a back | ||
| 62 | // navigation restores this page from the back/forward cache. GET forms are left | ||
| 63 | // alone: they search and filter, and do not need a wait state. A form with | ||
| 64 | // hx-post runs through HTMX, which has its own disabled-element handling, and a | ||
| 65 | // form with data-no-busy opts out. | ||
| 66 | (() => { | ||
| 67 | const DELAY = 150; | ||
| 68 | |||
| 69 | const reset = (root = document) => { | ||
| 70 | if (!root || typeof root.querySelectorAll !== 'function') return; | ||
| 71 | root.querySelectorAll('button[data-loading]').forEach((button) => { | ||
| 72 | if (button.loadingTimer) { | ||
| 73 | clearTimeout(button.loadingTimer); | ||
| 74 | button.loadingTimer = 0; | ||
| 75 | } | ||
| 76 | button.removeAttribute('data-loading'); | ||
| 77 | button.removeAttribute('aria-busy'); | ||
| 78 | button.classList.remove('spinning'); | ||
| 79 | button.disabled = false; | ||
| 80 | const mark = button.firstElementChild; | ||
| 81 | if (mark && mark.classList.contains('spinner')) mark.remove(); | ||
| 82 | }); | ||
| 83 | }; | ||
| 84 | |||
| 85 | const begin = (button) => { | ||
| 86 | button.disabled = true; | ||
| 87 | button.setAttribute('aria-busy', 'true'); | ||
| 88 | // Set the marker now so a cancel that arrives before the spinner is due can | ||
| 89 | // still find and restore the button. Only the spinner waits for DELAY. | ||
| 90 | button.setAttribute('data-loading', ''); | ||
| 91 | button.loadingTimer = setTimeout(() => { | ||
| 92 | button.loadingTimer = 0; | ||
| 93 | const mark = document.createElement('span'); | ||
| 94 | mark.className = 'spinner'; | ||
| 95 | mark.setAttribute('aria-hidden', 'true'); | ||
| 96 | button.classList.add('spinning'); | ||
| 97 | button.insertBefore(mark, button.firstChild); | ||
| 98 | }, DELAY); | ||
| 99 | }; | ||
| 100 | |||
| 101 | document.addEventListener('submit', (event) => { | ||
| 102 | const form = event.target; | ||
| 103 | if (!(form instanceof HTMLFormElement)) return; | ||
| 104 | if (form.method.toLowerCase() !== 'post') return; | ||
| 105 | if (form.hasAttribute('data-no-busy')) return; | ||
| 106 | if (form.hasAttribute('hx-post') || form.hasAttribute('hx-put') || | ||
| 107 | form.hasAttribute('hx-patch') || form.hasAttribute('hx-delete')) return; | ||
| 108 | // The confirmation handler above has already had its say. Checking here also | ||
| 109 | // covers any other listener that runs before this one and takes over. | ||
| 110 | if (event.defaultPrevented) return; | ||
| 111 | const buttons = Array.from(form.querySelectorAll('button[type="submit"], button:not([type])')) | ||
| 112 | .filter((button) => !button.disabled); | ||
| 113 | if (!buttons.length) return; | ||
| 114 | buttons.forEach(begin); | ||
| 115 | // A listener registered after this one may still cancel the submission. | ||
| 116 | // If it does, put the buttons back rather than leaving them disabled. | ||
| 117 | setTimeout(() => { | ||
| 118 | if (event.defaultPrevented) reset(form); | ||
| 119 | }, 0); | ||
| 120 | }); | ||
| 121 | |||
| 122 | window.addEventListener('pageshow', () => reset()); | ||
| 123 | })(); | ||
| 124 | |||
| 55 | 125 | // Syntax highlighting. The server sends each blob and each diff side as plain | |
| 56 | 126 | // lines, so the page is complete and scrollable before this runs; the tokens come | |
| 57 | 127 | // from a worker (static/prism-worker.js) so a large file never blocks the main | |