2021-06-12 03:51:07 +00:00
{{ template "base.html" . }}
{{ define "extrahead" }}
2021-06-13 17:22:55 +00:00
< script src = "{{ static " go_wasm_exec . js " } } " > < / script >
2021-09-22 19:18:39 +00:00
< script src = "{{ static " js / base64 . js " } } " > < / script >
2021-06-13 17:22:55 +00:00
< script >
2021-07-30 22:32:19 +00:00
const previewWorker = new Worker('/assets/editorpreviews.js');
2021-06-13 17:22:55 +00:00
< / script >
2021-07-20 01:12:27 +00:00
< style >
#editor {
resize: vertical;
}
2021-09-21 23:13:11 +00:00
#editor.drop {
box-shadow: inset 0px 0px 5px yellow;
}
2021-07-20 01:12:27 +00:00
< / style >
2021-06-12 03:51:07 +00:00
{{ end }}
{{ define "content" }}
2021-07-20 01:12:27 +00:00
< div class = "content-block ph3 ph0-ns" >
2021-07-30 22:32:19 +00:00
{{ if not .CanEditTitle }}
2021-07-22 02:16:10 +00:00
< h2 > {{ .Title }}< / h2 >
2021-07-20 02:35:22 +00:00
{{ end }}
2021-07-20 01:12:27 +00:00
< div class = "flex flex-column flex-row-ns" >
2021-09-01 06:15:13 +00:00
< form id = "form" action = "{{ .SubmitUrl }}" method = "post" class = "flex-fair-ns overflow-hidden" >
2021-07-20 01:12:27 +00:00
{{ csrftoken .Session }}
2021-07-30 22:32:19 +00:00
{{ if .CanEditTitle }}
< input id = "title" class = "b w-100 mb1" name = "title" type = "text" placeholder = "Post title..." value = "{{ .Title }}" / >
2021-07-20 02:35:22 +00:00
{{ end }}
2021-07-20 01:12:27 +00:00
{{/* 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 >
*/}}
2021-07-30 22:32:19 +00:00
< textarea id = "editor" class = "w-100 h6 minh-6 pa2 mono lh-copy" name = "body" > {{ .EditInitialContents }}< / textarea >
2021-06-12 03:51:07 +00:00
2021-09-22 10:59:03 +00:00
< div class = "flex justify-end items-center mt2" >
< div class = "upload_bar flex-grow-1" >
< div class = "instructions" >
Upload files by dragging & dropping, pasting, or < label class = "pointer link" for = "file_input" > selecting< / label > them.
< / div >
< div class = "progress flex" >
< div class = "progress_text mr3" > < / div >
< div class = "progress_bar flex-grow-1 flex-shrink-1 pa1" > < div class = "" > < / div > < / div >
< / div >
< / div >
< input type = "submit" class = "button ml2 flex-grow-0 flex-shrink-0" name = "submit" value = "{{ .SubmitLabel }}" / >
2021-07-20 01:12:27 +00:00
< / div >
2021-06-13 17:22:55 +00:00
2021-07-20 01:12:27 +00:00
{{ 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 %}
*/}}
2021-07-20 02:35:22 +00:00
{{ with .PostReplyingTo }}
< h4 class = "mt3" > The post you're replying to:< / h4 >
2021-09-01 06:15:13 +00:00
< div class = "mh-6 overflow-y-auto" >
{{ template "forum_post_standalone.html" . }}
< / div >
2021-07-20 02:35:22 +00:00
{{ end }}
2021-07-20 01:12:27 +00:00
{{/*
{% 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 >
2021-09-01 06:15:13 +00:00
< 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 >
2021-06-13 17:22:55 +00:00
< / div >
2021-09-22 10:59:03 +00:00
< input type = "file" multiple name = "file_input" id = "file_input" class = "dn" / > {{/* NOTE(asaf): Placing this outside the form to avoid submitting it to the server by accident */}}
2021-07-20 01:12:27 +00:00
< / div >
2021-06-12 03:51:07 +00:00
< / div >
2021-06-13 17:22:55 +00:00
< script >
2021-09-21 23:13:11 +00:00
const maxFileSize = {{ .MaxFileSize }};
const uploadUrl = {{ .UploadUrl }};
2021-07-20 00:52:50 +00:00
const form = document.querySelector('#form');
2021-07-22 01:48:52 +00:00
const titleField = document.querySelector('#title'); // may be undefined, be careful!
const textField = document.querySelector('#editor');
2021-06-13 17:22:55 +00:00
const preview = document.querySelector('#preview');
2021-07-20 00:52:50 +00:00
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);
2021-07-22 01:48:52 +00:00
if (storedContents & & !textField.value) {
2021-07-20 00:52:50 +00:00
try {
2021-07-22 01:48:52 +00:00
const { title, contents } = JSON.parse(storedContents);
if (titleField) {
titleField.value = title;
}
textField.value = contents;
2021-07-20 00:52:50 +00:00
} catch (e) {
console.error(e);
}
}
2021-07-17 23:20:47 +00:00
function updatePreview(previewHtml) {
2021-06-13 17:22:55 +00:00
preview.innerHTML = previewHtml;
2021-06-20 19:06:22 +00:00
MathJax.typeset();
2021-06-16 04:04:01 +00:00
}
2021-07-17 23:20:47 +00:00
previewWorker.onmessage = ({ data }) => {
updatePreview(data);
};
2021-07-20 00:52:50 +00:00
function doMarkdown() {
2021-07-22 01:48:52 +00:00
const md = textField.value;
2021-07-20 00:52:50 +00:00
previewWorker.postMessage(md);
2021-07-22 01:48:52 +00:00
updateContentCache();
}
function updateContentCache() {
2021-07-20 00:52:50 +00:00
window.localStorage.setItem(storageKey, JSON.stringify({
when: new Date().getTime(),
2021-07-22 01:48:52 +00:00
title: titleField ? titleField.value : '',
contents: textField.value,
2021-07-20 00:52:50 +00:00
}));
}
doMarkdown();
2021-07-22 01:48:52 +00:00
textField.addEventListener('input', () => doMarkdown());
if (titleField) {
titleField.addEventListener('input', () => updateContentCache());
}
2021-07-20 00:52:50 +00:00
form.addEventListener('submit', e => {
window.localStorage.removeItem(storageKey);
2021-06-16 04:04:01 +00:00
});
2021-09-21 23:13:11 +00:00
2021-09-22 10:59:03 +00:00
/*
/ Asset upload
*/
const submitButton = document.querySelector("#form input[type=submit]");
const submitText = submitButton.value;
const fileInput = document.querySelector('#file_input');
const uploadBar = document.querySelector('.upload_bar');
const uploadProgress = document.querySelector('.upload_bar .progress');
const uploadProgressText = document.querySelector('.upload_bar .progress_text');
const uploadProgressBar = document.querySelector('.upload_bar .progress_bar');
const uploadProgressBarFill = document.querySelector('.upload_bar .progress_bar > div');
2021-09-21 23:13:11 +00:00
let fileCounter = 0;
2021-09-22 10:59:03 +00:00
let enterCounter = 0;
let uploadQueue = [];
let currentUpload = null;
let currentXhr = null;
let currentBatchSize = 0;
let currentBatchDone = 0;
fileInput.addEventListener("change", function(ev) {
if (fileInput.files.length > 0) {
importUserFiles(fileInput.files);
}
});
2021-09-21 23:13:11 +00:00
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();
});
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");
2021-09-22 10:59:03 +00:00
if (ev.dataTransfer & & ev.dataTransfer.files) {
importUserFiles(ev.dataTransfer.files)
}
ev.preventDefault();
});
textField.addEventListener("paste", function(ev) {
if (ev.clipboardData & & ev.clipboardData.files) {
importUserFiles(ev.clipboardData.files)
}
});
function importUserFiles(files) {
2021-09-21 23:13:11 +00:00
let items = [];
2021-09-22 10:59:03 +00:00
for (let i = 0; i < files.length ; + + i ) {
let f = files[i];
2021-09-21 23:13:11 +00:00
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;
2021-09-22 10:59:03 +00:00
let toInsert = "";
let linesToCursor = textField.value.substr(0, cursorStart).split("\n");
let cursorLine = linesToCursor[linesToCursor.length-1].trim();
if (cursorLine.length > 0) {
toInsert = "\n";
}
2021-09-21 23:13:11 +00:00
for (let i = 0; i < items.length ; + + i ) {
if (items[i].file) {
fileCounter++;
toInsert += makeUploadString(fileCounter, items[i].file.name) + "\n";
2021-09-22 19:18:39 +00:00
queueUpload(fileCounter, items[i].file);
2021-09-21 23:13:11 +00:00
} else {
toInsert += `${items[i].error}\n`;
}
}
textField.value = textField.value.substring(0, cursorStart) + toInsert + textField.value.substring(cursorEnd, textField.value.length);
doMarkdown();
2021-09-22 19:18:39 +00:00
uploadNext();
2021-09-22 10:59:03 +00:00
}
2021-09-21 23:13:11 +00:00
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();
}
2021-09-22 19:18:39 +00:00
function replaceUploadStringError(upload) {
replaceUploadString(upload, `There was a problem uploading your file \`${upload.file.name}\`.`);
}
function queueUpload(uploadNumber, file) {
2021-09-21 23:13:11 +00:00
uploadQueue.push({
uploadNumber: uploadNumber,
file: file
});
2021-09-22 10:59:03 +00:00
currentBatchSize++;
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
2021-09-21 23:13:11 +00:00
}
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 {
2021-09-22 19:18:39 +00:00
replaceUploadStringError(currentUpload);
2021-09-21 23:13:11 +00:00
}
} else {
2021-09-22 19:18:39 +00:00
replaceUploadStringError(currentUpload);
2021-09-21 23:13:11 +00:00
}
} catch (err) {
console.error(err);
2021-09-22 19:18:39 +00:00
replaceUploadStringError(currentUpload);
2021-09-21 23:13:11 +00:00
}
currentUpload = null;
currentXhr = null;
2021-09-22 10:59:03 +00:00
currentBatchDone++;
2021-09-21 23:13:11 +00:00
uploadNext();
}
2021-09-22 10:59:03 +00:00
function updateUploadProgress(ev) {
if (ev.lengthComputable) {
let progress = ev.loaded / ev.total;
uploadProgressBarFill.style.width = Math.floor(progress * 100) + "%";
}
}
2021-09-21 23:13:11 +00:00
function uploadNext() {
if (currentUpload == null) {
next = uploadQueue.shift();
if (next) {
2021-09-22 10:59:03 +00:00
uploadProgressText.textContent = `Uploading files ${currentBatchDone+1}/${currentBatchSize}`;
uploadBar.classList.add("uploading");
uploadProgressBarFill.style.width = "0%";
submitButton.disabled = true;
submitButton.value = "Uploading files...";
2021-09-22 19:18:39 +00:00
try {
let utf8Filename = strToUTF8Arr(next.file.name);
let base64Filename = base64EncArr(utf8Filename);
// 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.upload.addEventListener("progress", updateUploadProgress);
currentXhr.open("POST", uploadUrl, true);
currentXhr.setRequestHeader("Hmn-Upload-Filename", base64Filename);
currentXhr.responseType = "json";
currentXhr.addEventListener("loadend", uploadDone);
currentXhr.send(next.file);
currentUpload = next;
} catch (err) {
replaceUploadStringError(next);
console.error(err);
uploadNext();
}
2021-09-22 10:59:03 +00:00
} else {
submitButton.disabled = false;
submitButton.value = submitText;
uploadBar.classList.remove("uploading");
currentBatchSize = 0;
currentBatchDone = 0;
2021-09-21 23:13:11 +00:00
}
}
}
2021-06-13 17:22:55 +00:00
< / script >
2021-06-12 03:51:07 +00:00
{{ end }}