339 lines
11 KiB
HTML
339 lines
11 KiB
HTML
{{ template "base.html" . }}
|
|
|
|
{{ define "extrahead" }}
|
|
<script src="{{ static "go_wasm_exec.js" }}"></script>
|
|
<script>
|
|
const previewWorker = new Worker('/assets/editorpreviews.js');
|
|
</script>
|
|
|
|
<style>
|
|
#editor {
|
|
resize: vertical;
|
|
}
|
|
|
|
#editor.drop {
|
|
box-shadow: inset 0px 0px 5px yellow;
|
|
}
|
|
</style>
|
|
{{ end }}
|
|
|
|
{{ define "content" }}
|
|
<div class="content-block ph3 ph0-ns">
|
|
{{ if not .CanEditTitle }}
|
|
<h2>{{ .Title }}</h2>
|
|
{{ end }}
|
|
<div class="flex flex-column flex-row-ns">
|
|
<form id="form" action="{{ .SubmitUrl }}" method="post" class="flex-fair-ns overflow-hidden">
|
|
{{ csrftoken .Session }}
|
|
|
|
{{ if .CanEditTitle }}
|
|
<input id="title" class="b w-100 mb1" name="title" type="text" placeholder="Post title..." value="{{ .Title }}" />
|
|
{{ end }}
|
|
{{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}}
|
|
{{/*
|
|
<div class="toolbar" id="toolbar">
|
|
<input type="button" id="bold" value="B" />
|
|
<input type="button" id="italic" value="I" />
|
|
<input type="button" id="underline" value="U" />
|
|
<input type="button" id="monospace" value="monospace" />
|
|
<input type="button" id="url" value="url" />
|
|
<input type="button" id="img" value="img" />
|
|
<input type="button" id="code" value="code" />
|
|
<input type="button" id="quote_simple" value="quote (anon)" />
|
|
<input type="button" id="quote_member" value="quote (member)" />
|
|
<input type="button" id="spoiler" value="spoiler" />
|
|
<input type="button" id="lalign" value="Left" />
|
|
<input type="button" id="calign" value="Center" />
|
|
<input type="button" id="ralign" value="Right" />
|
|
<input type="button" id="ulist" value="ul" />
|
|
<input type="button" id="olist" value="ol" />
|
|
<input type="button" id="litem" value="li" />
|
|
<input type="button" id="youtube" value="youtube" />
|
|
</div>
|
|
*/}}
|
|
<textarea id="editor" class="w-100 h6 minh-6 pa2 mono lh-copy" name="body">{{ .EditInitialContents }}</textarea>
|
|
|
|
<div class="flex flex-row-reverse justify-start mt2">
|
|
<input type="submit" class="button ml2" name="submit" value="{{ .SubmitLabel }}" />
|
|
</div>
|
|
|
|
{{ if .IsEditing }}
|
|
<span class="editreason">
|
|
<label for="editreason">Edit reason:</label>
|
|
<input name="editreason" maxlength="255" type="text" id="editreason" />
|
|
</span>
|
|
{{ end }}
|
|
|
|
{{/* TODO: Sticky threads
|
|
{% if user.is_staff and post and post.depth == 0 %}
|
|
<div class="checkbox sticky">
|
|
<input type="checkbox" name="sticky" id="sticky" {% if thread.sticky %}checked{% endif%} />
|
|
<label for="sticky">Sticky thread</label>
|
|
</div>
|
|
{% endif %}
|
|
*/}}
|
|
|
|
{{ with .PostReplyingTo }}
|
|
<h4 class="mt3">The post you're replying to:</h4>
|
|
<div class="mh-6 overflow-y-auto">
|
|
{{ template "forum_post_standalone.html" . }}
|
|
</div>
|
|
{{ end }}
|
|
|
|
{{/*
|
|
|
|
{% if context_newer %}
|
|
<h4>Replies since then:</h4>
|
|
<div class="recent-posts">
|
|
{% for post in posts_newer %}
|
|
{% include "forum_thread_single_post.html" %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if context_older %}
|
|
<h4>Replies before then:</h4>
|
|
<div class="recent-posts">
|
|
{% for post in posts_older %}
|
|
{% include "forum_thread_single_post.html" %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
*/}}
|
|
</form>
|
|
<div id="preview-container" class="post post-preview mathjax flex-fair-ns overflow-auto mv3 mv0-ns ml3-ns pa3 br3 bg--dim">
|
|
<div id="preview" class="post-content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const maxFileSize = {{ .MaxFileSize }};
|
|
const uploadUrl = {{ .UploadUrl }};
|
|
|
|
const form = document.querySelector('#form');
|
|
const titleField = document.querySelector('#title'); // may be undefined, be careful!
|
|
const textField = document.querySelector('#editor');
|
|
const preview = document.querySelector('#preview');
|
|
|
|
const storagePrefix = 'post-contents';
|
|
|
|
// Delete old irrelevant local post contents
|
|
const aWeekAgo = new Date().getTime() - (7 * 24 * 60 * 60 * 1000);
|
|
for (const key in window.localStorage) {
|
|
if (!window.localStorage.hasOwnProperty(key)) {
|
|
continue;
|
|
}
|
|
|
|
if (key.startsWith(storagePrefix)) {
|
|
try {
|
|
const { when } = JSON.parse(window.localStorage.getItem(key));
|
|
if (when <= aWeekAgo) {
|
|
window.localStorage.removeItem(key);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load any stored content from localStorage
|
|
const storageKey = `${storagePrefix}/${window.location.host}${window.location.pathname}`;
|
|
const storedContents = window.localStorage.getItem(storageKey);
|
|
if (storedContents && !textField.value) {
|
|
try {
|
|
const { title, contents } = JSON.parse(storedContents);
|
|
if (titleField) {
|
|
titleField.value = title;
|
|
}
|
|
textField.value = contents;
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
function updatePreview(previewHtml) {
|
|
preview.innerHTML = previewHtml;
|
|
MathJax.typeset();
|
|
}
|
|
|
|
previewWorker.onmessage = ({ data }) => {
|
|
updatePreview(data);
|
|
};
|
|
|
|
function doMarkdown() {
|
|
const md = textField.value;
|
|
previewWorker.postMessage(md);
|
|
updateContentCache();
|
|
}
|
|
|
|
function updateContentCache() {
|
|
window.localStorage.setItem(storageKey, JSON.stringify({
|
|
when: new Date().getTime(),
|
|
title: titleField ? titleField.value : '',
|
|
contents: textField.value,
|
|
}));
|
|
}
|
|
|
|
doMarkdown();
|
|
textField.addEventListener('input', () => doMarkdown());
|
|
if (titleField) {
|
|
titleField.addEventListener('input', () => updateContentCache());
|
|
}
|
|
|
|
form.addEventListener('submit', e => {
|
|
window.localStorage.removeItem(storageKey);
|
|
});
|
|
|
|
let fileCounter = 0;
|
|
|
|
textField.addEventListener("dragover", function(ev) {
|
|
let effect = "none";
|
|
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
|
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
|
effect = "copy";
|
|
break;
|
|
}
|
|
}
|
|
ev.dataTransfer.dropEffect = effect;
|
|
ev.preventDefault();
|
|
});
|
|
|
|
let enterCounter = 0;
|
|
|
|
textField.addEventListener("dragenter", function(ev) {
|
|
enterCounter++;
|
|
let droppable = false;
|
|
for (let i = 0; i < ev.dataTransfer.items.length; ++i) {
|
|
if (ev.dataTransfer.items[i].kind.toLowerCase() == "file") {
|
|
droppable = true;
|
|
break;
|
|
}
|
|
}
|
|
if (droppable) {
|
|
textField.classList.add("drop");
|
|
}
|
|
});
|
|
|
|
textField.addEventListener("dragleave", function(ev) {
|
|
enterCounter--;
|
|
if (enterCounter == 0) {
|
|
textField.classList.remove("drop");
|
|
}
|
|
});
|
|
|
|
function makeUploadString(uploadNumber, filename) {
|
|
return `Uploading file #${uploadNumber}: \`${filename}\`...`;
|
|
}
|
|
|
|
textField.addEventListener("drop", function(ev) {
|
|
enterCounter = 0;
|
|
textField.classList.remove("drop");
|
|
|
|
let items = [];
|
|
for (let i = 0; i < ev.dataTransfer.files.length; ++i) {
|
|
let f = ev.dataTransfer.files[i];
|
|
if (f.size < maxFileSize) {
|
|
items.push({ file: f, error: null });
|
|
} else {
|
|
items.push({ file: null, error: `\`${f.name}\` is too big! Max size is ${maxFileSize} but the file is ${f.size}.` });
|
|
}
|
|
}
|
|
|
|
let cursorStart = textField.selectionStart;
|
|
let cursorEnd = textField.selectionEnd;
|
|
|
|
let toInsert = "\n";
|
|
for (let i = 0; i < items.length; ++i) {
|
|
if (items[i].file) {
|
|
fileCounter++;
|
|
toInsert += makeUploadString(fileCounter, items[i].file.name) + "\n";
|
|
startUpload(fileCounter, items[i].file);
|
|
} else {
|
|
toInsert += `${items[i].error}\n`;
|
|
}
|
|
}
|
|
|
|
textField.value = textField.value.substring(0, cursorStart) + toInsert + textField.value.substring(cursorEnd, textField.value.length);
|
|
doMarkdown();
|
|
|
|
ev.preventDefault();
|
|
});
|
|
|
|
function replaceUploadString(upload, newString) {
|
|
let cursorStart = textField.selectionStart;
|
|
let cursorEnd = textField.selectionEnd;
|
|
let uploadString = makeUploadString(upload.uploadNumber, upload.file.name);
|
|
let insertIndex = textField.value.indexOf(uploadString)
|
|
textField.value = textField.value.replace(uploadString, newString);
|
|
if (cursorStart <= insertIndex + uploadString.length) {
|
|
textField.selectionStart = cursorStart;
|
|
} else {
|
|
textField.selectionStart = cursorStart - uploadString.length + newString.length;
|
|
}
|
|
if (cursorEnd <= insertIndex + uploadString.length) {
|
|
textField.selectionEnd = cursorEnd;
|
|
} else {
|
|
textField.selectionEnd = cursorEnd - uploadString.length + newString.length;
|
|
}
|
|
doMarkdown();
|
|
}
|
|
|
|
let uploadQueue = [];
|
|
let currentUpload = null;
|
|
let currentXhr = null;
|
|
|
|
function startUpload(uploadNumber, file) {
|
|
uploadQueue.push({
|
|
uploadNumber: uploadNumber,
|
|
file: file
|
|
});
|
|
uploadNext();
|
|
}
|
|
|
|
function uploadDone(ev) {
|
|
try {
|
|
if (currentXhr.status == 200 && currentXhr.response) {
|
|
if (currentXhr.response.url) {
|
|
let url = currentXhr.response.url;
|
|
let newString = `[${currentUpload.file.name}](${url})`;
|
|
if (currentXhr.response.mime.startsWith("image")) {
|
|
newString = "!" + newString;
|
|
}
|
|
|
|
replaceUploadString(currentUpload, newString);
|
|
} else if (currentXhr.response.error) {
|
|
replaceUploadString(currentUpload, `Upload failed for \`${currentUpload.file.name}\`: ${currentXhr.response.error}.`);
|
|
} else {
|
|
replaceUploadString(currentUpload, `There was a problem uploading your file \`${currentUpload.file.name}\`.`);
|
|
}
|
|
} else {
|
|
replaceUploadString(currentUpload, `There was a problem uploading your file \`${currentUpload.file.name}\`.`);
|
|
}
|
|
} catch (err) {
|
|
replaceUploadString(currentUpload, `There was a problem uploading your file \`${currentUpload.file.name}\`.`);
|
|
console.error(err);
|
|
}
|
|
currentUpload = null;
|
|
currentXhr = null;
|
|
uploadNext();
|
|
}
|
|
|
|
function uploadNext() {
|
|
if (currentUpload == null) {
|
|
next = uploadQueue.shift();
|
|
if (next) {
|
|
// NOTE(asaf): We use XHR because fetch can't do upload progress reports. Womp womp. https://youtu.be/Pubd-spHN-0?t=2
|
|
currentXhr = new XMLHttpRequest();
|
|
currentXhr.open("POST", uploadUrl, true);
|
|
currentXhr.setRequestHeader("Hmn-Upload-Filename", next.file.name);
|
|
currentXhr.responseType = "json";
|
|
currentXhr.addEventListener("loadend", uploadDone);
|
|
currentXhr.send(next.file);
|
|
currentUpload = next;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{{ end }}
|