HTML · 17668 bytes Raw Blame History
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 }}