improvements
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package frontend
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates
|
||||
var templates embed.FS
|
||||
|
||||
func readFilename(filename string) string {
|
||||
f, err := templates.ReadFile(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(f)
|
||||
}
|
||||
|
||||
func EditPage() string {
|
||||
return readFilename("templates/edit.html.tmpl")
|
||||
}
|
||||
|
||||
func AdminPage() string {
|
||||
return readFilename("templates/admin.html.tmpl")
|
||||
}
|
||||
|
||||
func AuthorPage() string {
|
||||
return readFilename("templates/author.html.tmpl")
|
||||
}
|
||||
|
||||
func UploadPage() string {
|
||||
return readFilename("templates/upload.html.tmpl")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<html><body>not implemented</body></html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
{{ $timeFormat := "Jan _2 15:04:05" }}
|
||||
<p>Create content:</p>
|
||||
<form action="/hugo/edit/new" method="POST">
|
||||
<label>Name: <input type="text" name="name" /></label>
|
||||
<select name="type">
|
||||
{{- range .ContentTypes }}
|
||||
<option value="{{ . }}">{{ . }}</option>
|
||||
{{- end }}
|
||||
</select>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
<p>Edit content:</p>
|
||||
<table>{{ range .Content }}
|
||||
<tr>
|
||||
{{ if .Metadata }}
|
||||
<td>
|
||||
<a href="/hugo/edit/{{ .Filename }}">
|
||||
{{ .Metadata.Title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ .Metadata.Date.Format $timeFormat }}
|
||||
{{ if not (.Metadata.Lastmod.Equal .Metadata.Date) }}
|
||||
(last modified {{.Metadata.Lastmod.Format $timeFormat }})
|
||||
{{end}}
|
||||
</td>
|
||||
{{ else }}
|
||||
<td>
|
||||
<a href="/hugo/edit/{{ .Filename }}">
|
||||
{{ .Filename }}
|
||||
</a>
|
||||
</td>
|
||||
<td>(unable to load metadata)</td>
|
||||
{{ end }}
|
||||
</tr>
|
||||
{{- end }}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,316 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="/hugo/simplemde.js"></script>
|
||||
<style type="text/css" media="screen">
|
||||
#editor-wrapper {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 0;
|
||||
bottom: 150px;
|
||||
left: 40%;
|
||||
}
|
||||
#draft {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 60%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
}
|
||||
#draft > iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/hugo/simplemde.css" />
|
||||
<script src="/hugo/vue.js"></script>
|
||||
<script src="/hugo/moment.js"></script>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="container" >
|
||||
<div id="header">
|
||||
<div id="lastSaved">
|
||||
<span v-if="sendQueue.length > 0 || Object.keys(needConfirmation).length > 0">last saved ${ lastSaved.from(now) }, saving</span>
|
||||
<span v-else>saved</span>
|
||||
<span v-if="connectionError">, ${connectionError}</span>
|
||||
</div>
|
||||
<div>
|
||||
<a id="sideview-toggle-media">media</a>
|
||||
<a id="sideview-toggle-draft">draft</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editor-wrapper">
|
||||
<textarea id="editor">{{ .LoadContent }}</textarea>
|
||||
</div>
|
||||
<div id="draft"><iframe src="{{ .IframeSource }}">Loading draft...</iframe></div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
var iframe = document.querySelector("#draft > iframe");
|
||||
|
||||
document.onclick = function (event) {
|
||||
var iframe = document.querySelector("#draft > iframe");
|
||||
switch (event.target.id) {
|
||||
case "sideview-toggle-media":
|
||||
iframe.src = "/hugo/media";
|
||||
break;
|
||||
case "sideview-toggle-draft":
|
||||
iframe.src = "{{ .IframeSource }}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var uiBindings = {
|
||||
ltime: {{ .LTime }},
|
||||
serverLtime: 0,
|
||||
lastSaved: moment(),
|
||||
now: moment(),
|
||||
connectionError: null,
|
||||
sendQueue: [],
|
||||
sentRecently: [],
|
||||
needConfirmation: {},
|
||||
};
|
||||
|
||||
var app = new Vue({
|
||||
el: "#container",
|
||||
data: uiBindings,
|
||||
delimiters: ["${", "}"],
|
||||
});
|
||||
|
||||
function getLtime() {
|
||||
uiBindings.ltime++
|
||||
return uiBindings.ltime
|
||||
}
|
||||
|
||||
function observeServer(l, confirmed) {
|
||||
uiBindings.serverLtime = l;
|
||||
if (confirmed && confirmed.length > 0) {
|
||||
confirmed.forEach(function (e) {
|
||||
delete uiBindings.needConfirmation[e];
|
||||
})
|
||||
}
|
||||
observe(l);
|
||||
}
|
||||
|
||||
function observe(l) {
|
||||
if (l > uiBindings.ltime) {
|
||||
uiBindings.now = moment();
|
||||
uiBindings.lastSaved = moment();
|
||||
uiBindings.ltime = l;
|
||||
}
|
||||
}
|
||||
|
||||
var selectedImage;
|
||||
var editorElem = document.getElementById("editor");
|
||||
var editor = new SimpleMDE({
|
||||
element: editorElem,
|
||||
forceSync: true,
|
||||
insertTexts: {
|
||||
image: ["{\{% thumb filename=\"", "#url#\" width=\"200\" %}}"]
|
||||
},
|
||||
imageURLFn: function () {
|
||||
return selectedImage;
|
||||
}
|
||||
});
|
||||
|
||||
window.onmessage = function (evt) {
|
||||
selectedImage = evt.data;
|
||||
}
|
||||
|
||||
|
||||
// Create WebSocket connection.
|
||||
var socket = connect();
|
||||
|
||||
|
||||
const sawChangesBumpsTo = 10;
|
||||
|
||||
var sentinelSrc = 'about:blank';
|
||||
var oldSrc = '';
|
||||
|
||||
var sawChanges = -1;
|
||||
window.setInterval(function () {
|
||||
if (sawChanges >= 0) {
|
||||
sawChanges--;
|
||||
if (sawChanges == 0) {
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiBindings.now = moment();
|
||||
if (uiBindings.connectionError) {
|
||||
socket = connect();
|
||||
} else if (uiBindings.sendQueue.length > 0) {
|
||||
var ltime = getLtime();
|
||||
|
||||
// record lowest pending
|
||||
// ltime at the time this message
|
||||
// was serialized
|
||||
var lowestPending = ltime;
|
||||
for (c in uiBindings.needConfirmation) {
|
||||
c = parseInt(c, 10);
|
||||
if (lowestPending === 0 || c < lowestPending) {
|
||||
lowestPending = c;
|
||||
}
|
||||
}
|
||||
|
||||
var msg = JSON.stringify({
|
||||
"deltas": uiBindings.sendQueue,
|
||||
"ltime": ltime,
|
||||
"lowestPending": lowestPending,
|
||||
});
|
||||
uiBindings.sendQueue = [];
|
||||
uiBindings.needConfirmation[ltime] = msg;
|
||||
}
|
||||
|
||||
for (ltime in uiBindings.needConfirmation) {
|
||||
var msg = uiBindings.needConfirmation[ltime];
|
||||
socket.send(msg);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
function connect() {
|
||||
const socket = new WebSocket((location.protocol == "https:" ? 'wss://' : 'ws://') + location.host + location.pathname);
|
||||
|
||||
// Listen for messages
|
||||
socket.addEventListener('message', function (event) {
|
||||
var message = JSON.parse(event.data);
|
||||
observeServer(message.ltime, message.confirmed);
|
||||
|
||||
var deltas = [];
|
||||
deltas.push.apply(deltas, message.deltas);
|
||||
|
||||
deltas.forEach(function(aceDelta) {
|
||||
var cmDelta = aceDeltaToCM(aceDelta)
|
||||
|
||||
var content = ""
|
||||
var to = {
|
||||
line: aceDelta.start.row,
|
||||
ch: aceDelta.start.column,
|
||||
}
|
||||
|
||||
if (aceDelta.action == "insert") {
|
||||
content = aceDelta.lines.join("\n");
|
||||
to = null;
|
||||
}
|
||||
|
||||
editor.codemirror.doc.replaceRange(content, cmDelta.from, to, "dontreflect");
|
||||
sawChanges = sawChangesBumpsTo;
|
||||
})
|
||||
});
|
||||
|
||||
socket.addEventListener('open', function () {
|
||||
uiBindings.connectionError = null;
|
||||
});
|
||||
|
||||
socket.addEventListener('close', function () {
|
||||
if (!uiBindings.connectionError) {
|
||||
getLtime();
|
||||
uiBindings.connectionError = "server connection closed, reconnecting...";
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener('error', function (err) {
|
||||
if (!uiBindings.connectionError) {
|
||||
uiBindings.connectionError = err;
|
||||
getLtime();
|
||||
}
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
editor.codemirror.on("change", function (cm, cmDelta) {
|
||||
if (cmDelta.origin == "dontreflect") {
|
||||
return;
|
||||
}
|
||||
|
||||
var aceDelta = cmDeltaToAce(cmDelta);
|
||||
console.log(cmDelta, "=>", aceDelta)
|
||||
|
||||
sawChanges = sawChangesBumpsTo;
|
||||
uiBindings.sendQueue.push.apply(uiBindings.sendQueue, aceDelta);
|
||||
})
|
||||
|
||||
function cmDeltaToAce(cmDelta) {
|
||||
var isRemove = (cmDelta.removed.length > 0 && cmDelta.removed[0].length > 0) || cmDelta.removed.length > 1;
|
||||
var lines = isRemove ? cmDelta.removed : cmDelta.text;
|
||||
var aceDelta = {
|
||||
action: isRemove ? "remove" : "insert",
|
||||
lines: lines,
|
||||
start: {
|
||||
row: cmDelta.from.line,
|
||||
column: cmDelta.from.ch,
|
||||
},
|
||||
end: {
|
||||
row: cmDelta.from.line + (isRemove ? lines.length - 1 : lines.length - 1 ),
|
||||
column: lines[lines.length-1].length,
|
||||
}
|
||||
};
|
||||
|
||||
if (aceDelta.start.row == aceDelta.end.row) {
|
||||
aceDelta.end.column += cmDelta.from.ch;
|
||||
}
|
||||
|
||||
if (false && isRemove && aceDelta.start.row == aceDelta.end.row) {
|
||||
var origStart = aceDelta.start;
|
||||
aceDelta.start = aceDelta.end;
|
||||
aceDelta.end = origStart;
|
||||
aceDelta.start.column += cmDelta.from.ch;
|
||||
}
|
||||
|
||||
if (isRemove && ((cmDelta.text.length > 0 && cmDelta.text[0].length > 0) || cmDelta.text.length > 1)) {
|
||||
cmDelta.removed = [""];
|
||||
var ret = [aceDelta];
|
||||
ret.push.apply(ret, cmDeltaToAce(cmDelta));
|
||||
return ret;
|
||||
}
|
||||
|
||||
return [aceDelta];
|
||||
}
|
||||
|
||||
function aceDeltaToCM(aceDelta) {
|
||||
|
||||
var cmDelta = {
|
||||
text: [],
|
||||
removed: [],
|
||||
from: {
|
||||
line: aceDelta.start.row,
|
||||
ch: aceDelta.start.column,
|
||||
},
|
||||
to: {
|
||||
// cm deltas are weird. to refers to the selection end, which
|
||||
// with a simple blinking cursor with no selection, is always
|
||||
// the same as from
|
||||
line: aceDelta.start.row,
|
||||
ch: aceDelta.start.column,
|
||||
},
|
||||
}
|
||||
|
||||
if (aceDelta.action == "remove") {
|
||||
var origStart = aceDelta.start;
|
||||
aceDelta.start = aceDelta.end;
|
||||
aceDelta.end = origStart;
|
||||
|
||||
cmDelta.removed = aceDelta.lines
|
||||
cmDelta.text = [""]
|
||||
} else {
|
||||
cmDelta.text = aceDelta.lines
|
||||
cmDelta.removed = [""]
|
||||
}
|
||||
|
||||
return cmDelta;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
<input type="file" style="display: hidden;" id="{{.ElemName}}" />
|
||||
<div id="{{.ElemName}}_dropzone" ondrop="dropHandler(event);" ondragover="draghandler(event);" ondragenter="draghandler(event);" ondragleave="draghandler(event);" style="background-color: rgba(0,0,0,0.5); visibility: hidden; opacity:0; position: fixed; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; ; transition: visibility 175ms, opacity 175ms; z-index: 9999999;"></div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var fileInput = document.getElementById('{{.ElemName}}');
|
||||
var dropzone = document.getElementById('{{.ElemName}}_dropzone');
|
||||
|
||||
fileInput.onchange = function () {
|
||||
var formData = new FormData();
|
||||
fileInput.files.forEach(function (file) {
|
||||
formData.append(file.name, file);
|
||||
});
|
||||
upload(formData);
|
||||
}
|
||||
|
||||
var lastTarget = null;
|
||||
|
||||
window.addEventListener("dragenter", function(e)
|
||||
{
|
||||
lastTarget = e.target; // cache the last target here
|
||||
// unhide our dropzone overlay
|
||||
dropzone.style.visibility = "";
|
||||
dropzone.style.opacity = 1;
|
||||
});
|
||||
|
||||
window.addEventListener("dragleave", function(e)
|
||||
{
|
||||
// this is the magic part. when leaving the window,
|
||||
// e.target happens to be exactly what we want: what we cached
|
||||
// at the start, the dropzone we dragged into.
|
||||
// so..if dragleave target matches our cache, we hide the dropzone.
|
||||
if(e.target === lastTarget)
|
||||
{
|
||||
dropzone.style.visibility = "hidden";
|
||||
dropzone.style.opacity = 0;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function draghandler(evt) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
function dropHandler(evt) {
|
||||
evt.preventDefault();
|
||||
|
||||
var files = evt.dataTransfer.files;
|
||||
var formData = new FormData();
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
formData.append(files[i].name, files[i]);
|
||||
}
|
||||
|
||||
upload(formData);
|
||||
return false;
|
||||
}
|
||||
|
||||
function upload(formData) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function(e) {
|
||||
if ( 4 == this.readyState ) {
|
||||
window.location.reload(true);
|
||||
}
|
||||
}
|
||||
xhr.open('POST', '/hugo/upload');
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user