aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/templates/partials/editor.html
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/templates/partials/editor.html')
-rw-r--r--internal/web/templates/partials/editor.html67
1 files changed, 52 insertions, 15 deletions
diff --git a/internal/web/templates/partials/editor.html b/internal/web/templates/partials/editor.html
index 33b2a1c..a0f8c7f 100644
--- a/internal/web/templates/partials/editor.html
+++ b/internal/web/templates/partials/editor.html
@@ -1,22 +1,30 @@
-{{define "editor"}}<div class="editor" id="ed-{{.name}}-wrap">
+{{define "editor"}}<div class="editor{{if eq .format "html"}} html{{end}}" id="ed-{{.name}}-wrap">
+<p class="ed-format" id="ed-{{.name}}-format">
+ <span class="muted">{{t "Format"}}:</span>
+ <label><input type="radio" name="format" value="markdown"{{if ne .format "html"}} checked{{end}}> Markdown</label>
+ <label><input type="radio" name="format" value="html"{{if eq .format "html"}} checked{{end}}> HTML</label>
+ <span class="muted small ed-htmlnote">{{t "Put on your blog exactly as written — embeds and scripts all work. An unclosed tag can break the page around it, so check your blog after saving."}}</span>
+</p>
<div class="ed-toolbar" id="ed-{{.name}}-bar" hidden>
- <span class="ed-group">
+ <span class="ed-group ed-md">
<button type="button" data-cmd="bold" title="{{t "Bold"}} (Ctrl+B)"><svg viewBox="0 0 16 16"><path d="M4 2h5a3.5 3.5 0 0 1 2.4 6 3.75 3.75 0 0 1-2.2 6.8H4zM6 4v3.5h3a1.75 1.75 0 0 0 0-3.5zm0 5.5V13h3.2a1.75 1.75 0 0 0 0-3.5z"/></svg></button>
<button type="button" data-cmd="italic" title="{{t "Italic"}} (Ctrl+I)"><svg viewBox="0 0 16 16"><path d="M6.5 2h6v2h-2l-2.5 8h2v2h-6v-2h2l2.5-8h-2z"/></svg></button>
<button type="button" data-cmd="strike" title="{{t "Strikethrough"}}"><svg viewBox="0 0 16 16"><path d="M2 7.5h12V9H2z"/><path d="M8 2c2.3 0 3.8 1 4.1 2.8l-1.9.3C10 4.2 9.2 3.7 8 3.7c-1.3 0-2 .5-2 1.3 0 .5.3.9 1.2 1.2H4.3C4 5.8 4 5.3 4 5c0-1.8 1.6-3 4-3zm3.9 8.4c0 1.9-1.6 3.1-4 3.1-2.5 0-4.1-1.2-4.4-3.2l1.9-.3c.2 1.1 1.1 1.7 2.5 1.7 1.3 0 2-.5 2-1.3 0-.2 0-.4-.1-.5h2c.1.2.1.4.1.5z"/></svg></button>
</span>
- <span class="ed-group">
+ <span class="ed-group ed-md">
<button type="button" data-cmd="heading" title="{{t "Heading"}}"><svg viewBox="0 0 16 16"><path d="M3 2h2v5h6V2h2v12h-2V9H5v5H3z"/></svg></button>
<button type="button" data-cmd="quote" title="{{t "Quote"}}"><svg viewBox="0 0 16 16"><path d="M3 4h4v4H5v1.5c0 .6.4 1 1 1h1V12H6a3 3 0 0 1-3-3zm6 0h4v4h-2v1.5c0 .6.4 1 1 1h1V12h-1a3 3 0 0 1-3-3z"/></svg></button>
<button type="button" data-cmd="code" title="{{t "Code"}}"><svg viewBox="0 0 16 16"><path d="M5.5 3.5 1 8l4.5 4.5 1.4-1.4L3.8 8l3.1-3.1zm5 0L9.1 4.9 12.2 8l-3.1 3.1 1.4 1.4L15 8z"/></svg></button>
</span>
- <span class="ed-group">
+ <span class="ed-group ed-md">
<button type="button" data-cmd="ul" title="{{t "Bulleted list"}}"><svg viewBox="0 0 16 16"><path d="M2 3h2v2H2zm4 0h8v2H6zM2 7h2v2H2zm4 0h8v2H6zm-4 4h2v2H2zm4 0h8v2H6z"/></svg></button>
<button type="button" data-cmd="ol" title="{{t "Numbered list"}}"><svg viewBox="0 0 16 16"><path d="M2.2 2h1.3v3.5H2.4v-2.3l-.7.4V2.5zM6 3h8v2H6zm-4.1 4.2c0-.8.6-1.3 1.5-1.3s1.4.5 1.4 1.2c0 .5-.3.9-.8 1.3l-.7.6h1.6v1H1.9v-.8L3.3 8c.3-.3.4-.5.4-.7 0-.3-.2-.4-.4-.4-.3 0-.5.2-.5.5zM6 7h8v2H6zm-4.2 4.1c0-.7.6-1.1 1.5-1.1.8 0 1.4.4 1.4 1 0 .4-.3.7-.7.8.5.1.8.5.8.9 0 .7-.6 1.2-1.5 1.2-.9 0-1.5-.4-1.6-1.1h1c0 .2.2.4.5.4s.5-.1.5-.4-.2-.4-.6-.4h-.3v-.8h.3c.3 0 .5-.1.5-.4 0-.2-.2-.3-.4-.3s-.4.1-.4.3zM6 11h8v2H6z"/></svg></button>
<button type="button" data-cmd="hr" title="{{t "Horizontal rule"}}"><svg viewBox="0 0 16 16"><path d="M2 7h12v2H2z"/></svg></button>
</span>
- <span class="ed-group">
+ <span class="ed-group ed-md">
<button type="button" data-cmd="link" title="{{t "Link"}} (Ctrl+K)"><svg viewBox="0 0 16 16"><path d="M6.4 9.6a3.5 3.5 0 0 0 4.9 0l2-2a3.5 3.5 0 0 0-4.9-5l-1 1 1.1 1.1 1-1a2 2 0 0 1 2.8 2.9l-2 2a2 2 0 0 1-2.9 0zm3.2-3.2a3.5 3.5 0 0 0-4.9 0l-2 2a3.5 3.5 0 0 0 4.9 5l1-1-1.1-1.1-1 1a2 2 0 0 1-2.8-2.9l2-2a2 2 0 0 1 2.9 0z"/></svg></button>
+ </span>
+ <span class="ed-group">
<button type="button" data-cmd="file" title="{{t "Insert file"}}"><svg viewBox="0 0 16 16"><path d="M10.5 1.5a3.5 3.5 0 0 1 2.5 6l-5.6 5.6a2.25 2.25 0 0 1-3.2-3.2L9.6 4.6 10.7 5.7l-5.4 5.3a.75.75 0 0 0 1.1 1.1L12 6.4a2 2 0 0 0-2.9-2.8L3.6 9.1 2.5 8l5.5-5.5a3.5 3.5 0 0 1 2.5-1z"/></svg></button>
</span>
<span class="ed-modes">
@@ -31,6 +39,7 @@
</div>
<textarea name="{{.name}}" id="ed-{{.name}}" rows="{{.rows}}"{{if .tall}} class="tall"{{end}}>{{.value}}</textarea>
<div class="ed-preview" id="ed-{{.name}}-preview" hidden></div>
+<iframe class="ed-preview-frame" id="ed-{{.name}}-frame" sandbox title="{{t "Preview"}}" hidden></iframe>
<div class="editor-tools">
<label class="upload">{{t "Insert file"}} <input type="file" name="inline_file" id="ed-{{.name}}-file"></label>
<span class="upload-status" id="ed-{{.name}}-status"></span>
@@ -42,10 +51,22 @@
<script>
(function () {
var id = "ed-{{.name}}", ta = document.getElementById(id), inp = document.getElementById(id + "-file"), st = document.getElementById(id + "-status"),
- wrap = document.getElementById(id + "-wrap"), bar = document.getElementById(id + "-bar"), lk = document.getElementById(id + "-link"), pv = document.getElementById(id + "-preview");
+ wrap = document.getElementById(id + "-wrap"), bar = document.getElementById(id + "-bar"), lk = document.getElementById(id + "-link"), pv = document.getElementById(id + "-preview"),
+ frame = document.getElementById(id + "-frame"), fmt = document.getElementById(id + "-format");
if (!window.fetch || !window.FormData || !ta || !inp) return; // old browser: plain textarea, the file is uploaded on save
bar.hidden = false;
- wrap.className = "editor js";
+
+ // ---- format: HTML mode keeps the toolbar to Insert file and Write/Preview
+ function isHTML() { var r = fmt.querySelector("input[value=html]"); return !!(r && r.checked); }
+ var previewing = false;
+ function setClass() { wrap.className = "editor js" + (isHTML() ? " html" : "") + (previewing ? " previewing" : ""); }
+ function setFormat() {
+ ta.spellcheck = !isHTML();
+ lk.hidden = true;
+ if (previewing) setMode(true); else setClass(); // re-render the preview the other way
+ }
+ fmt.addEventListener("change", setFormat);
+ setFormat();
// ---- text primitives: everything goes through replace() so Ctrl+Z works
function setSel(s, e) { ta.focus(); ta.setSelectionRange(s, e); }
@@ -103,11 +124,13 @@
link: openLink,
file: function () { inp.click(); }
};
+ var htmlCmds = { file: true }; // the only command that makes sense in HTML mode
+ function run(name) { if (cmds[name] && (!isHTML() || htmlCmds[name])) cmds[name](); }
bar.addEventListener("click", function (ev) {
var b = ev.target.closest("button");
if (!b) return;
if (b.getAttribute("data-mode")) setMode(b.getAttribute("data-mode") === "preview");
- else if (cmds[b.getAttribute("data-cmd")]) cmds[b.getAttribute("data-cmd")]();
+ else run(b.getAttribute("data-cmd"));
});
// ---- link box: replaces window.prompt, keeps the selection it was opened on
@@ -140,10 +163,10 @@
ta.addEventListener("keydown", function (ev) {
if ((ev.ctrlKey || ev.metaKey) && !ev.altKey) {
var k = { b: "bold", i: "italic", k: "link" }[ev.key.toLowerCase()];
- if (k) { ev.preventDefault(); cmds[k](); }
+ if (k && !isHTML()) { ev.preventDefault(); cmds[k](); }
return;
}
- if (ev.key !== "Enter" || ev.shiftKey || ta.selectionStart !== ta.selectionEnd) return;
+ if (ev.key !== "Enter" || ev.shiftKey || isHTML() || ta.selectionStart !== ta.selectionEnd) return;
var s = ta.selectionStart, v = ta.value, ls = v.lastIndexOf("\n", s - 1) + 1, m = /^(\s*)([-*+] (?:\[[ x]\] )?|(\d+)\. )(.*)$/.exec(v.slice(ls, s));
if (!m) return;
ev.preventDefault();
@@ -152,14 +175,28 @@
replace(s, s, "\n" + m[1] + mark);
});
- // ---- preview: the server renders exactly what a save would
+ // ---- preview: the server renders Markdown exactly as a save would. HTML is
+ // shown in a sandboxed frame (opaque origin, no scripts): the text may not be
+ // the viewer's own — a superadmin edits other people's blogs — and must never
+ // run in the dashboard.
var modes = bar.querySelectorAll("[data-mode]");
function setMode(preview) {
- wrap.className = "editor js" + (preview ? " previewing" : "");
+ previewing = preview;
+ setClass();
for (var i = 0; i < modes.length; i++) modes[i].setAttribute("aria-pressed", String((modes[i].getAttribute("data-mode") === "preview") === preview));
lk.hidden = true;
- if (!preview) { pv.hidden = true; ta.hidden = false; ta.focus(); return; }
- pv.style.minHeight = ta.offsetHeight + "px";
+ if (!preview) { pv.hidden = true; frame.hidden = true; ta.hidden = false; ta.focus(); return; }
+ var h = ta.offsetHeight || pv.offsetHeight || frame.offsetHeight;
+ if (isHTML()) {
+ pv.hidden = true;
+ frame.style.height = h + "px";
+ frame.srcdoc = ta.value;
+ frame.hidden = false; ta.hidden = true;
+ st.textContent = "";
+ return;
+ }
+ frame.hidden = true;
+ pv.style.minHeight = h + "px";
pv.innerHTML = "";
pv.hidden = false; ta.hidden = true;
st.textContent = {{t "Rendering…"}};
@@ -181,7 +218,7 @@
fd.append("file", file);
return fetch({{.upload}}, { method: "POST", body: fd, credentials: "same-origin", headers: { Accept: "application/json" } })
.then(function (r) { return r.json().then(function (j) { if (!r.ok || j.error) throw new Error(j.error || {{t "upload failed"}}); return j; }); })
- .then(function (j) { insertLine(j.markdown); st.textContent = {{t "Inserted"}} + " " + j.filename + "."; inp.value = ""; })
+ .then(function (j) { insertLine(isHTML() ? j.html : j.markdown); st.textContent = {{t "Inserted"}} + " " + j.filename + "."; inp.value = ""; })
.catch(function (err) { st.textContent = {{t "Could not upload:"}} + " " + err.message; });
}
function uploadAll(files) { // one after the other, so each lands where the caret is