| 1 | {{ define "page" -}} |
| 2 | <section class="shithub-repo-page shithub-editor-page"> |
| 3 | <main class="shithub-editor" data-code-editor data-editor-mode="{{ .Mode }}"> |
| 4 | {{ with .Error }}<p class="shithub-flash shithub-flash-error" role="alert">{{ . }}</p>{{ end }} |
| 5 | {{ with .Notice }}<p class="shithub-flash shithub-flash-notice" role="status">{{ . }}</p>{{ end }} |
| 6 | |
| 7 | <form method="post" action="{{ .FormAction }}" class="shithub-editor-form"{{ if eq .Mode "upload" }} enctype="multipart/form-data"{{ end }}> |
| 8 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> |
| 9 | <input type="hidden" name="base_oid" value="{{ .BaseOID }}"> |
| 10 | |
| 11 | <header class="shithub-editor-filebar"> |
| 12 | <div class="shithub-editor-filepath"> |
| 13 | <span class="shithub-editor-file-icon" aria-hidden="true">{{ octicon "file" }}</span> |
| 14 | <a href="/{{ .Owner }}/{{ .Repo.Name }}" class="shithub-editor-repo-name">{{ .Repo.Name }}</a> |
| 15 | {{ if eq .Mode "upload" }} |
| 16 | <span class="shithub-code-sep">/</span> |
| 17 | <span class="shithub-editor-static-path">Upload files</span> |
| 18 | {{ else if eq .Mode "delete" }} |
| 19 | <span class="shithub-code-sep">/</span> |
| 20 | <span class="shithub-editor-static-path">{{ .Path }}</span> |
| 21 | {{ else }} |
| 22 | <span class="shithub-code-sep">/</span> |
| 23 | <input type="text" name="path" value="{{ .PathValue }}" aria-label="File path" autocomplete="off" spellcheck="false" data-editor-path> |
| 24 | {{ end }} |
| 25 | <span class="shithub-editor-in">in</span> |
| 26 | <span class="shithub-editor-branch">{{ .RefDisplay }}</span> |
| 27 | </div> |
| 28 | <div class="shithub-editor-top-actions"> |
| 29 | <a href="{{ .CancelURL }}" class="shithub-button shithub-button-large" data-editor-cancel>Cancel changes</a> |
| 30 | <button type="button" class="shithub-button shithub-button-primary shithub-button-large" data-editor-commit-open disabled>{{ .Primary }}...</button> |
| 31 | </div> |
| 32 | </header> |
| 33 | |
| 34 | {{ if eq .Mode "upload" }} |
| 35 | <section class="shithub-editor-panel"> |
| 36 | <div class="shithub-editor-panel-head"> |
| 37 | <h2>Upload files</h2> |
| 38 | <span>{{ if .UploadDir }}{{ .UploadDir }}{{ else }}/{{ end }}</span> |
| 39 | </div> |
| 40 | <label class="shithub-upload-drop"> |
| 41 | {{ octicon "upload" }} |
| 42 | <span>Choose files</span> |
| 43 | <input type="file" name="files" multiple required data-upload-input> |
| 44 | </label> |
| 45 | <ul class="shithub-upload-list" data-upload-list></ul> |
| 46 | </section> |
| 47 | {{ else if eq .Mode "delete" }} |
| 48 | <section class="shithub-editor-panel shithub-editor-danger"> |
| 49 | <div class="shithub-editor-panel-head"> |
| 50 | <h2>Delete this file</h2> |
| 51 | <span>{{ .Path }}</span> |
| 52 | </div> |
| 53 | <p>This commit removes <code>{{ .Path }}</code> from <code>{{ .RefDisplay }}</code>.</p> |
| 54 | </section> |
| 55 | {{ else }} |
| 56 | <section class="shithub-editor-panel"> |
| 57 | <div class="shithub-editor-toolbar"> |
| 58 | <div class="shithub-editor-tabs" role="tablist" aria-label="File editor tabs"> |
| 59 | <button type="button" class="is-active" role="tab" aria-selected="true" data-editor-tab="edit">Edit</button> |
| 60 | <button type="button" role="tab" aria-selected="false" data-editor-tab="preview">Preview</button> |
| 61 | </div> |
| 62 | <div class="shithub-editor-tools" data-editor-edit-tools> |
| 63 | <details class="shithub-editor-menu" data-editor-menu> |
| 64 | <summary><span data-indent-mode-label>Spaces</span>{{ octicon "triangle-down" }}</summary> |
| 65 | <div class="shithub-editor-menu-panel" role="menu"> |
| 66 | <strong>Indent mode</strong> |
| 67 | <button type="button" class="is-selected" data-indent-mode-option="spaces" role="menuitemradio" aria-checked="true"><span class="shithub-editor-menu-check" aria-hidden="true"></span>Spaces</button> |
| 68 | <button type="button" data-indent-mode-option="tabs" role="menuitemradio" aria-checked="false"><span class="shithub-editor-menu-check" aria-hidden="true"></span>Tabs</button> |
| 69 | </div> |
| 70 | </details> |
| 71 | <details class="shithub-editor-menu" data-editor-menu> |
| 72 | <summary><span data-indent-size-label>2</span>{{ octicon "triangle-down" }}</summary> |
| 73 | <div class="shithub-editor-menu-panel" role="menu"> |
| 74 | <strong>Indent size</strong> |
| 75 | <button type="button" class="is-selected" data-indent-size-option="2" role="menuitemradio" aria-checked="true"><span class="shithub-editor-menu-check" aria-hidden="true"></span>2</button> |
| 76 | <button type="button" data-indent-size-option="4" role="menuitemradio" aria-checked="false"><span class="shithub-editor-menu-check" aria-hidden="true"></span>4</button> |
| 77 | <button type="button" data-indent-size-option="8" role="menuitemradio" aria-checked="false"><span class="shithub-editor-menu-check" aria-hidden="true"></span>8</button> |
| 78 | </div> |
| 79 | </details> |
| 80 | <details class="shithub-editor-menu" data-editor-menu> |
| 81 | <summary><span data-wrap-label>Soft wrap</span>{{ octicon "triangle-down" }}</summary> |
| 82 | <div class="shithub-editor-menu-panel shithub-editor-menu-panel-wide" role="menu"> |
| 83 | <strong>Line wrap mode</strong> |
| 84 | <button type="button" data-wrap-option="off" role="menuitemradio" aria-checked="false"><span class="shithub-editor-menu-check" aria-hidden="true"></span>No wrap</button> |
| 85 | <button type="button" class="is-selected" data-wrap-option="soft" role="menuitemradio" aria-checked="true"><span class="shithub-editor-menu-check" aria-hidden="true"></span>Soft wrap</button> |
| 86 | </div> |
| 87 | </details> |
| 88 | </div> |
| 89 | <label class="shithub-editor-diff-toggle" data-editor-preview-tools hidden> |
| 90 | <input type="checkbox" checked data-editor-show-diff> |
| 91 | <span>Show Diff</span> |
| 92 | </label> |
| 93 | </div> |
| 94 | <div class="shithub-editor-pane" data-editor-pane="edit"> |
| 95 | <div class="shithub-editor-textbox"> |
| 96 | <pre class="shithub-editor-gutter" aria-hidden="true" data-editor-gutter>1</pre> |
| 97 | <textarea name="content" spellcheck="false" autocomplete="off" autocapitalize="off" data-editor-content>{{ .Content }}</textarea> |
| 98 | </div> |
| 99 | <div class="shithub-editor-helpbar"> |
| 100 | Use <kbd>Control + Shift + m</kbd> to toggle the <kbd>tab</kbd> key moving focus. Alternatively, use <kbd>esc</kbd> then <kbd>tab</kbd> to move to the next interactive element on the page. |
| 101 | </div> |
| 102 | </div> |
| 103 | <div class="shithub-editor-pane shithub-editor-preview" data-editor-pane="preview" hidden> |
| 104 | <div class="markdown-body shithub-editor-preview-body" data-editor-preview data-preview-url="{{ .PreviewURL }}" data-preview-ref="{{ .Ref }}"> |
| 105 | <p class="shithub-editor-preview-empty">Nothing to preview.</p> |
| 106 | </div> |
| 107 | </div> |
| 108 | </section> |
| 109 | {{ end }} |
| 110 | |
| 111 | <dialog class="shithub-commit-dialog" data-editor-commit-dialog> |
| 112 | <div class="shithub-commit-dialog-head"> |
| 113 | <h2>Commit changes</h2> |
| 114 | <button type="button" class="shithub-commit-dialog-close" aria-label="Close" data-editor-commit-close>{{ octicon "x" }}</button> |
| 115 | </div> |
| 116 | <label class="shithub-form-row"> |
| 117 | <span>Commit message</span> |
| 118 | <input type="text" name="commit_message" value="{{ .Message }}" required> |
| 119 | </label> |
| 120 | <label class="shithub-form-row"> |
| 121 | <span>Extended description</span> |
| 122 | <textarea name="commit_description" rows="4">{{ .Description }}</textarea> |
| 123 | </label> |
| 124 | <div class="shithub-commit-target">{{ octicon "git-commit" }} Commit directly to the <strong>{{ .RefDisplay }}</strong> branch.</div> |
| 125 | <div class="shithub-form-actions"> |
| 126 | <button type="button" class="shithub-button" data-editor-commit-close>Cancel</button> |
| 127 | <button type="submit" class="shithub-button{{ if eq .Mode "delete" }} shithub-button-danger{{ else }} shithub-button-primary{{ end }}" data-editor-commit-submit>{{ .Primary }}</button> |
| 128 | </div> |
| 129 | </dialog> |
| 130 | </form> |
| 131 | </main> |
| 132 | </section> |
| 133 | |
| 134 | <script> |
| 135 | (function () { |
| 136 | const root = document.querySelector("[data-code-editor]"); |
| 137 | if (!root) return; |
| 138 | const textarea = root.querySelector("[data-editor-content]"); |
| 139 | const gutter = root.querySelector("[data-editor-gutter]"); |
| 140 | const pathInput = root.querySelector("[data-editor-path]"); |
| 141 | const preview = root.querySelector("[data-editor-preview]"); |
| 142 | const previewTools = root.querySelector("[data-editor-preview-tools]"); |
| 143 | const editTools = root.querySelector("[data-editor-edit-tools]"); |
| 144 | const showDiff = root.querySelector("[data-editor-show-diff]"); |
| 145 | const commitOpen = root.querySelector("[data-editor-commit-open]"); |
| 146 | const commitSubmit = root.querySelector("[data-editor-commit-submit]"); |
| 147 | const commitDialog = root.querySelector("[data-editor-commit-dialog]"); |
| 148 | const commitMessage = root.querySelector("input[name='commit_message']"); |
| 149 | const uploadInput = root.querySelector("[data-upload-input]"); |
| 150 | const uploadList = root.querySelector("[data-upload-list]"); |
| 151 | const mode = root.dataset.editorMode || "edit"; |
| 152 | let indentMode = "spaces"; |
| 153 | let indentSize = 2; |
| 154 | let softWrap = true; |
| 155 | let previewDirty = true; |
| 156 | let tabMovesFocus = false; |
| 157 | let nextTabMovesFocus = false; |
| 158 | let submitting = false; |
| 159 | |
| 160 | function updateGutter() { |
| 161 | if (!textarea || !gutter) return; |
| 162 | const count = Math.max(1, textarea.value.split("\n").length); |
| 163 | let lines = ""; |
| 164 | for (let i = 1; i <= count; i++) lines += i + "\n"; |
| 165 | gutter.textContent = lines; |
| 166 | } |
| 167 | |
| 168 | function dirty() { |
| 169 | if (mode === "delete") return true; |
| 170 | if (uploadInput) return uploadInput.files && uploadInput.files.length > 0; |
| 171 | const contentChanged = textarea && textarea.value !== (textarea.dataset.original || ""); |
| 172 | const pathChanged = pathInput && pathInput.value !== (pathInput.dataset.original || ""); |
| 173 | if (mode === "new") return !!(pathInput && pathInput.value.trim()) && (!!(textarea && textarea.value) || pathChanged); |
| 174 | return !!(contentChanged || pathChanged); |
| 175 | } |
| 176 | |
| 177 | function updateCommitState() { |
| 178 | const changed = dirty(); |
| 179 | [commitOpen, commitSubmit].forEach(function (button) { |
| 180 | if (button) button.disabled = !changed; |
| 181 | }); |
| 182 | } |
| 183 | |
| 184 | function setMenuSelection(selector, value, labelSelector, label) { |
| 185 | root.querySelectorAll(selector).forEach(function (button) { |
| 186 | const selected = button.dataset.indentModeOption === value || button.dataset.indentSizeOption === value || button.dataset.wrapOption === value; |
| 187 | button.classList.toggle("is-selected", selected); |
| 188 | button.setAttribute("aria-checked", selected ? "true" : "false"); |
| 189 | }); |
| 190 | const labelEl = root.querySelector(labelSelector); |
| 191 | if (labelEl) labelEl.textContent = label; |
| 192 | } |
| 193 | |
| 194 | function applyEditorOptions() { |
| 195 | if (!textarea) return; |
| 196 | textarea.style.tabSize = String(indentSize); |
| 197 | gutter.style.tabSize = String(indentSize); |
| 198 | root.classList.toggle("is-soft-wrap", softWrap); |
| 199 | } |
| 200 | |
| 201 | function closeEditorMenus() { |
| 202 | root.querySelectorAll("[data-editor-menu]").forEach(function (menu) { |
| 203 | menu.removeAttribute("open"); |
| 204 | }); |
| 205 | } |
| 206 | |
| 207 | async function renderPreview() { |
| 208 | if (!textarea || !preview || !previewDirty) return; |
| 209 | const body = new URLSearchParams(); |
| 210 | body.set("csrf_token", root.querySelector("input[name='csrf_token']").value); |
| 211 | body.set("content", textarea.value); |
| 212 | body.set("ref", preview.dataset.previewRef || ""); |
| 213 | body.set("path", pathInput ? pathInput.value : ""); |
| 214 | if (pathInput) body.set("original_path", pathInput.dataset.original || pathInput.value || ""); |
| 215 | if (showDiff && showDiff.checked) body.set("show_diff", "1"); |
| 216 | preview.innerHTML = "<p class=\"shithub-editor-preview-empty\">Rendering preview...</p>"; |
| 217 | const res = await fetch(preview.dataset.previewUrl, { |
| 218 | method: "POST", |
| 219 | headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Requested-With": "XMLHttpRequest" }, |
| 220 | body: body.toString() |
| 221 | }); |
| 222 | preview.innerHTML = res.ok ? await res.text() : "<p class=\"shithub-editor-preview-empty\">Preview failed.</p>"; |
| 223 | previewDirty = false; |
| 224 | } |
| 225 | |
| 226 | function openCommitDialog() { |
| 227 | if (!commitDialog || (commitOpen && commitOpen.disabled)) return; |
| 228 | if (commitDialog.showModal) { |
| 229 | commitDialog.showModal(); |
| 230 | } else { |
| 231 | commitDialog.setAttribute("open", ""); |
| 232 | } |
| 233 | if (commitMessage) commitMessage.focus(); |
| 234 | } |
| 235 | |
| 236 | function closeCommitDialog() { |
| 237 | if (!commitDialog) return; |
| 238 | if (commitDialog.close) { |
| 239 | commitDialog.close(); |
| 240 | } else { |
| 241 | commitDialog.removeAttribute("open"); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | root.addEventListener("submit", function () { submitting = true; }); |
| 246 | window.addEventListener("beforeunload", function (event) { |
| 247 | if (submitting || !dirty()) return; |
| 248 | event.preventDefault(); |
| 249 | event.returnValue = ""; |
| 250 | }); |
| 251 | |
| 252 | if (textarea) { |
| 253 | textarea.dataset.original = textarea.value; |
| 254 | if (pathInput) pathInput.dataset.original = pathInput.value; |
| 255 | updateGutter(); |
| 256 | applyEditorOptions(); |
| 257 | updateCommitState(); |
| 258 | textarea.addEventListener("input", function () { |
| 259 | updateGutter(); |
| 260 | nextTabMovesFocus = false; |
| 261 | previewDirty = true; |
| 262 | updateCommitState(); |
| 263 | }); |
| 264 | textarea.addEventListener("scroll", function () { |
| 265 | if (gutter) gutter.scrollTop = textarea.scrollTop; |
| 266 | }); |
| 267 | textarea.addEventListener("keydown", function (event) { |
| 268 | if ((event.key === "m" || event.key === "M") && event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey) { |
| 269 | event.preventDefault(); |
| 270 | tabMovesFocus = !tabMovesFocus; |
| 271 | nextTabMovesFocus = false; |
| 272 | return; |
| 273 | } |
| 274 | if (event.key === "Escape") { |
| 275 | nextTabMovesFocus = true; |
| 276 | return; |
| 277 | } |
| 278 | if (event.key !== "Tab") { |
| 279 | nextTabMovesFocus = false; |
| 280 | return; |
| 281 | } |
| 282 | if (tabMovesFocus || nextTabMovesFocus) { |
| 283 | nextTabMovesFocus = false; |
| 284 | return; |
| 285 | } |
| 286 | event.preventDefault(); |
| 287 | const start = textarea.selectionStart; |
| 288 | const end = textarea.selectionEnd; |
| 289 | const insert = indentMode === "tabs" ? "\t" : " ".repeat(indentSize); |
| 290 | textarea.setRangeText(insert, start, end, "end"); |
| 291 | updateGutter(); |
| 292 | previewDirty = true; |
| 293 | updateCommitState(); |
| 294 | }); |
| 295 | } |
| 296 | |
| 297 | root.querySelectorAll("[data-editor-tab]").forEach(function (button) { |
| 298 | button.addEventListener("click", function () { |
| 299 | const tab = button.dataset.editorTab; |
| 300 | root.querySelectorAll("[data-editor-tab]").forEach(function (b) { |
| 301 | const active = b === button; |
| 302 | b.classList.toggle("is-active", active); |
| 303 | b.setAttribute("aria-selected", active ? "true" : "false"); |
| 304 | }); |
| 305 | root.querySelectorAll("[data-editor-pane]").forEach(function (pane) { |
| 306 | pane.hidden = pane.dataset.editorPane !== tab; |
| 307 | }); |
| 308 | if (editTools) editTools.hidden = tab !== "edit"; |
| 309 | if (previewTools) previewTools.hidden = tab !== "preview"; |
| 310 | if (pathInput) pathInput.readOnly = tab === "preview"; |
| 311 | root.classList.toggle("is-previewing", tab === "preview"); |
| 312 | if (tab === "preview") renderPreview(); |
| 313 | }); |
| 314 | }); |
| 315 | |
| 316 | if (pathInput) { |
| 317 | pathInput.dataset.original = pathInput.value; |
| 318 | pathInput.addEventListener("input", function () { |
| 319 | previewDirty = true; |
| 320 | updateCommitState(); |
| 321 | }); |
| 322 | } |
| 323 | |
| 324 | root.querySelectorAll("[data-indent-mode-option]").forEach(function (button) { |
| 325 | button.addEventListener("click", function () { |
| 326 | indentMode = button.dataset.indentModeOption || "spaces"; |
| 327 | setMenuSelection("[data-indent-mode-option]", indentMode, "[data-indent-mode-label]", indentMode === "tabs" ? "Tabs" : "Spaces"); |
| 328 | closeEditorMenus(); |
| 329 | }); |
| 330 | }); |
| 331 | |
| 332 | root.querySelectorAll("[data-indent-size-option]").forEach(function (button) { |
| 333 | button.addEventListener("click", function () { |
| 334 | indentSize = parseInt(button.dataset.indentSizeOption || "2", 10); |
| 335 | setMenuSelection("[data-indent-size-option]", String(indentSize), "[data-indent-size-label]", String(indentSize)); |
| 336 | applyEditorOptions(); |
| 337 | closeEditorMenus(); |
| 338 | }); |
| 339 | }); |
| 340 | |
| 341 | root.querySelectorAll("[data-wrap-option]").forEach(function (button) { |
| 342 | button.addEventListener("click", function () { |
| 343 | softWrap = button.dataset.wrapOption !== "off"; |
| 344 | setMenuSelection("[data-wrap-option]", softWrap ? "soft" : "off", "[data-wrap-label]", softWrap ? "Soft wrap" : "No wrap"); |
| 345 | applyEditorOptions(); |
| 346 | closeEditorMenus(); |
| 347 | }); |
| 348 | }); |
| 349 | |
| 350 | if (showDiff) { |
| 351 | showDiff.addEventListener("change", function () { |
| 352 | previewDirty = true; |
| 353 | if (root.classList.contains("is-previewing")) renderPreview(); |
| 354 | }); |
| 355 | } |
| 356 | |
| 357 | if (commitOpen) { |
| 358 | commitOpen.addEventListener("click", openCommitDialog); |
| 359 | } |
| 360 | |
| 361 | root.querySelectorAll("[data-editor-commit-close]").forEach(function (button) { |
| 362 | button.addEventListener("click", closeCommitDialog); |
| 363 | }); |
| 364 | |
| 365 | if (commitDialog) { |
| 366 | commitDialog.addEventListener("click", function (event) { |
| 367 | if (event.target === commitDialog) closeCommitDialog(); |
| 368 | }); |
| 369 | } |
| 370 | |
| 371 | if (uploadInput && uploadList) { |
| 372 | uploadInput.addEventListener("change", function () { |
| 373 | uploadList.innerHTML = ""; |
| 374 | Array.from(uploadInput.files || []).forEach(function (file) { |
| 375 | const item = document.createElement("li"); |
| 376 | item.textContent = file.name + " · " + file.size + " bytes"; |
| 377 | uploadList.appendChild(item); |
| 378 | }); |
| 379 | updateCommitState(); |
| 380 | }); |
| 381 | } |
| 382 | updateCommitState(); |
| 383 | }()); |
| 384 | </script> |
| 385 | {{- end }} |