Add editor interface
This commit is contained in:
50
assets/editor.js
Normal file
50
assets/editor.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
(function () {
|
||||||
|
var textarea = document.getElementById('editor');
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
function wrap(before, after, placeholder) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var end = textarea.selectionEnd;
|
||||||
|
var selected = textarea.value.slice(start, end) || placeholder;
|
||||||
|
var replacement = before + selected + after;
|
||||||
|
textarea.value = textarea.value.slice(0, start) + replacement + textarea.value.slice(end);
|
||||||
|
if (selected === placeholder) {
|
||||||
|
textarea.selectionStart = start + before.length;
|
||||||
|
textarea.selectionEnd = start + before.length + placeholder.length;
|
||||||
|
} else {
|
||||||
|
textarea.selectionStart = start + replacement.length;
|
||||||
|
textarea.selectionEnd = start + replacement.length;
|
||||||
|
}
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function linePrefix(prefix) {
|
||||||
|
var start = textarea.selectionStart;
|
||||||
|
var lineStart = textarea.value.lastIndexOf('\n', start - 1) + 1;
|
||||||
|
textarea.value = textarea.value.slice(0, lineStart) + prefix + textarea.value.slice(lineStart);
|
||||||
|
textarea.selectionStart = textarea.selectionEnd = start + prefix.length;
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions = {
|
||||||
|
bold: function () { wrap('**', '**', 'bold text'); },
|
||||||
|
italic: function () { wrap('*', '*', 'italic text'); },
|
||||||
|
h1: function () { linePrefix('# '); },
|
||||||
|
h2: function () { linePrefix('## '); },
|
||||||
|
h3: function () { linePrefix('### '); },
|
||||||
|
code: function () { wrap('`', '`', 'code'); },
|
||||||
|
codeblock: function () { wrap('```\n', '\n```', 'code'); },
|
||||||
|
quote: function () { linePrefix('> '); },
|
||||||
|
link: function () { wrap('[', '](url)', 'link text'); },
|
||||||
|
ul: function () { linePrefix('- '); },
|
||||||
|
ol: function () { linePrefix('1. '); },
|
||||||
|
hr: function () { wrap('\n\n---\n\n', '', ''); },
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-action]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var action = actions[btn.dataset.action];
|
||||||
|
if (action) action();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -18,12 +18,30 @@
|
|||||||
<main>
|
<main>
|
||||||
{{if .EditMode}}
|
{{if .EditMode}}
|
||||||
<form class="edit-form" method="POST" action="{{.PostURL}}">
|
<form class="edit-form" method="POST" action="{{.PostURL}}">
|
||||||
<textarea name="content" autofocus>{{.RawContent}}</textarea>
|
<div class="editor-toolbar">
|
||||||
|
<button type="button" class="btn-tool" data-action="bold" title="Bold">**</button>
|
||||||
|
<button type="button" class="btn-tool" data-action="italic" title="Italic">*</button>
|
||||||
|
<span class="toolbar-sep"></span>
|
||||||
|
<button type="button" class="btn-tool" data-action="h1" title="Heading 1">#</button>
|
||||||
|
<button type="button" class="btn-tool" data-action="h2" title="Heading 2">##</button>
|
||||||
|
<button type="button" class="btn-tool" data-action="h3" title="Heading 3">###</button>
|
||||||
|
<span class="toolbar-sep"></span>
|
||||||
|
<button type="button" class="btn-tool" data-action="code" title="Inline code">`</button>
|
||||||
|
<button type="button" class="btn-tool" data-action="codeblock" title="Code block">```</button>
|
||||||
|
<span class="toolbar-sep"></span>
|
||||||
|
<button type="button" class="btn-tool" data-action="link" title="Link">[]</button>
|
||||||
|
<button type="button" class="btn-tool" data-action="quote" title="Blockquote">></button>
|
||||||
|
<button type="button" class="btn-tool" data-action="ul" title="Unordered list">-</button>
|
||||||
|
<button type="button" class="btn-tool" data-action="ol" title="Ordered list">1.</button>
|
||||||
|
<button type="button" class="btn-tool" data-action="hr" title="Horizontal rule">---</button>
|
||||||
|
</div>
|
||||||
|
<textarea name="content" id="editor" autofocus>{{.RawContent}}</textarea>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<a class="btn-cancel" href="{{.PostURL}}">CANCEL</a>
|
<a class="btn-cancel" href="{{.PostURL}}">CANCEL</a>
|
||||||
<button class="btn-save" type="submit">SAVE</button>
|
<button class="btn-save" type="submit">SAVE</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<script src="/_/editor.js"></script>
|
||||||
{{else}} {{if .Content}}
|
{{else}} {{if .Content}}
|
||||||
<div class="content">{{.Content}}</div>
|
<div class="content">{{.Content}}</div>
|
||||||
{{end}} {{if .Entries}}
|
{{end}} {{if .Entries}}
|
||||||
|
|||||||
@@ -233,11 +233,52 @@ main {
|
|||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Editor toolbar === */
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
border: 1px solid #0a0;
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
background: #001a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-tool {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #0f0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 0.15rem;
|
||||||
|
text-shadow: 0 0 4px #0a0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-tool::before {
|
||||||
|
content: "[";
|
||||||
|
color: #060;
|
||||||
|
}
|
||||||
|
.btn-tool::after {
|
||||||
|
content: "]";
|
||||||
|
color: #060;
|
||||||
|
}
|
||||||
|
.btn-tool:hover {
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 6px #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-sep {
|
||||||
|
width: 1px;
|
||||||
|
background: #060;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
/* === Edit form === */
|
/* === Edit form === */
|
||||||
.edit-form {
|
.edit-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
@@ -245,7 +286,10 @@ textarea {
|
|||||||
min-height: 60vh;
|
min-height: 60vh;
|
||||||
background: #000;
|
background: #000;
|
||||||
border: 1px solid #0a0;
|
border: 1px solid #0a0;
|
||||||
color: white;
|
border-top: none;
|
||||||
|
color: #0f0;
|
||||||
|
caret-color: #0f0;
|
||||||
|
text-shadow: 0 0 4px #0a0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -86,7 +86,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
urlPath := path.Clean("/" + r.URL.Path)
|
urlPath := path.Clean("/" + r.URL.Path)
|
||||||
fsPath := filepath.Join(h.root, filepath.FromSlash(urlPath))
|
|
||||||
|
fsPath := filepath.Join(h.root, filepath.FromSlash(urlPath))
|
||||||
|
|
||||||
// Security: ensure the resolved path stays within root.
|
// Security: ensure the resolved path stays within root.
|
||||||
rel, err := filepath.Rel(h.root, fsPath)
|
rel, err := filepath.Rel(h.root, fsPath)
|
||||||
|
|||||||
Reference in New Issue
Block a user