572 lines
23 KiB
ERB
572 lines
23 KiB
ERB
<%
|
|
crumbs = @mode == :new ? [["Posts", to("/admin/")], ["New post", nil]] : [["Posts", to("/admin/")], [@post.title.to_s.empty? ? "Untitled" : @post.title, nil]]
|
|
%>
|
|
|
|
<div class="page-head">
|
|
<div>
|
|
<h1><%= @mode == :new ? "New post" : "Edit post" %></h1>
|
|
<p class="lede"><%= @mode == :new ? "Draft a new article in Markdown." : "Update and publish this post." %></p>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem;">
|
|
<% if @mode == :edit %>
|
|
<a class="btn btn-ghost" href="<%= to("/api/volumen/posts/#{@post.slug}") %>" target="_blank">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 14px; height: 14px;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
|
Preview
|
|
</a>
|
|
<% end %>
|
|
<a class="btn btn-ghost" href="<%= to("/admin/") %>">Cancel</a>
|
|
<button type="submit" form="post-form" class="btn btn-primary">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width: 14px; height: 14px;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<% if @error %>
|
|
<div class="alert alert-error">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<div><%= h(@error) %></div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<form id="post-form" method="post"
|
|
action="<%= @mode == :new ? to("/admin/posts") : h(to("/admin/posts/#{@post.slug}")) %>">
|
|
<input type="hidden" name="_csrf" value="<%= csrf_token %>">
|
|
|
|
<div class="form-grid">
|
|
<div class="surface full">
|
|
<div class="surface-head">
|
|
<div>
|
|
<h2>Details</h2>
|
|
<p class="lede">Title, slug, and publishing options</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field full">
|
|
<label for="title">Title</label>
|
|
<input id="title" class="input" type="text" name="title" value="<%= h(@post.title) %>" placeholder="A clear, descriptive title" required>
|
|
</div>
|
|
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="slug">Slug</label>
|
|
<input id="slug" class="input" type="text" name="slug" value="<%= h(@post.slug) %>"
|
|
<%= @mode == :edit ? "readonly" : "required" %> placeholder="my-post-slug">
|
|
</div>
|
|
<div class="field">
|
|
<label for="lang">Language</label>
|
|
<input id="lang" class="input" type="text" name="lang" value="<%= h(@post.lang) %>" placeholder="en">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-row">
|
|
<div class="field">
|
|
<label for="author">Author</label>
|
|
<input id="author" class="input" type="text" name="author" value="<%= h(@post.author) %>" placeholder="Petr Balvín">
|
|
</div>
|
|
<div class="field">
|
|
<label for="fediverse_creator">Fediverse creator</label>
|
|
<input id="fediverse_creator" class="input" type="text" name="fediverse_creator"
|
|
value="<%= h(@post.fediverse_creator) %>" placeholder="@user@instance.tld">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field-row-3">
|
|
<div class="field">
|
|
<label for="date">Publish date</label>
|
|
<input id="date" class="input" type="date" name="date" value="<%= h(@post.date_string || Date.today.iso8601) %>">
|
|
</div>
|
|
<div class="field">
|
|
<label for="publish_at">Schedule for</label>
|
|
<input id="publish_at" class="input" type="date" name="publish_at"
|
|
value="<%= @post.publish_at.is_a?(Date) ? @post.publish_at.strftime("%Y-%m-%d") : "" %>">
|
|
<p class="help">Leave empty to publish immediately</p>
|
|
</div>
|
|
<div class="field">
|
|
<label for="tags">Tags</label>
|
|
<input id="tags" class="input" type="text" name="tags" value="<%= h(@post.tags.join(", ")) %>" placeholder="comma, separated">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field full">
|
|
<label for="excerpt">Excerpt</label>
|
|
<input id="excerpt" class="input" type="text" name="excerpt" value="<%= h(@post.excerpt) %>" placeholder="Short summary for listings and previews">
|
|
<p class="help">Auto-generated from the first paragraph if left empty</p>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap; margin-top: 0.5rem;">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="draft" <%= @post.draft? ? "checked" : "" %>> Draft
|
|
</label>
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="all_langs" <%= @post.all_langs? ? "checked" : "" %>> Available in all languages
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="surface full">
|
|
<div class="surface-head">
|
|
<div>
|
|
<h2>Cover image</h2>
|
|
<p class="lede">Header image shown on the article page</p>
|
|
</div>
|
|
</div>
|
|
<div class="cover-field">
|
|
<div class="cover-input-row">
|
|
<input id="cover-input" class="input" type="text" name="cover" value="<%= h(@post.cover) %>"
|
|
placeholder="/media/… or https://…">
|
|
<button type="button" id="cover-upload" class="btn">Upload</button>
|
|
<button type="button" id="cover-clear" class="btn btn-ghost">Clear</button>
|
|
</div>
|
|
<input id="cover-alt-input" class="input" type="text" name="cover_alt" value="<%= h(@post.cover_alt) %>"
|
|
placeholder="ALT text (for accessibility)">
|
|
<input id="cover-caption-input" class="input" type="text" name="cover_caption" value="<%= h(@post.cover_caption) %>"
|
|
placeholder="Caption (e.g. 'Generated by AI')">
|
|
<img id="cover-preview" class="cover-preview" alt="" hidden>
|
|
<input type="file" id="cover-file" accept="image/*" hidden>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="surface full">
|
|
<div class="editor-tabs" style="margin-bottom: 0.75rem;">
|
|
<button type="button" data-tab="markdown" class="active">Markdown</button>
|
|
<button type="button" data-tab="visual">Visual</button>
|
|
</div>
|
|
|
|
<div id="visual-view" hidden>
|
|
<div class="editor-grid">
|
|
<div class="editor-pane">
|
|
<div class="editor-head">
|
|
<span class="label">Editor</span>
|
|
<span class="spacer"></span>
|
|
<button type="button" class="editor-toolbar-btn bold" data-rich="bold" title="Bold (Ctrl+B)">B</button>
|
|
<button type="button" class="editor-toolbar-btn italic" data-rich="italic" title="Italic (Ctrl+I)">I</button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="h2" title="Heading 2">H2</button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="h3" title="Heading 3">H3</button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="link" title="Link (Ctrl+K)">⌘</button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="ul" title="Bullet list">•</button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="ol" title="Numbered list">1.</button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="quote" title="Quote">"</button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="code" title="Code">/></button>
|
|
<button type="button" class="editor-toolbar-btn" data-rich="image" title="Image">IMG</button>
|
|
</div>
|
|
<div id="wysiwyg" class="markdown editor-preview" contenteditable="true"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="markdown-view">
|
|
<div class="editor-grid" id="editor-grid">
|
|
<div class="editor-pane">
|
|
<div class="editor-head">
|
|
<span class="label">Markdown</span>
|
|
<span class="spacer"></span>
|
|
<button type="button" class="editor-toolbar-btn bold" data-md="bold" title="Bold (Ctrl+B)">B</button>
|
|
<button type="button" class="editor-toolbar-btn italic" data-md="italic" title="Italic (Ctrl+I)">I</button>
|
|
<button type="button" class="editor-toolbar-btn" data-md="h2" title="Heading">H2</button>
|
|
<button type="button" class="editor-toolbar-btn" data-md="link" title="Link (Ctrl+K)">↗</button>
|
|
<button type="button" class="editor-toolbar-btn" data-md="ul" title="List">•</button>
|
|
<button type="button" class="editor-toolbar-btn" data-md="quote" title="Quote">"</button>
|
|
<button type="button" class="editor-toolbar-btn" data-md="code" title="Code"></></button>
|
|
<button type="button" class="editor-toolbar-btn" data-md="image" title="Image">IMG</button>
|
|
<button type="button" id="toggle-preview" class="editor-toolbar-btn" title="Toggle preview">PREVIEW</button>
|
|
</div>
|
|
<textarea id="body" name="body" class="editor-textarea" rows="22" placeholder="Write your post in Markdown…"><%= h(@post.body) %></textarea>
|
|
</div>
|
|
<div class="editor-pane" id="preview-pane">
|
|
<div class="editor-head">
|
|
<span class="label">Preview</span>
|
|
</div>
|
|
<div id="preview" class="markdown editor-preview"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="file" id="image-input" accept="image/*" hidden>
|
|
</form>
|
|
|
|
<script>
|
|
(function () {
|
|
var form = document.getElementById("post-form");
|
|
var textarea = document.getElementById("body");
|
|
var preview = document.getElementById("preview");
|
|
var wysiwyg = document.getElementById("wysiwyg");
|
|
var visualView = document.getElementById("visual-view");
|
|
var markdownView = document.getElementById("markdown-view");
|
|
var imageInput = document.getElementById("image-input");
|
|
var coverInput = document.getElementById("cover-input");
|
|
var coverFile = document.getElementById("cover-file");
|
|
var coverPreview = document.getElementById("cover-preview");
|
|
var previewUrl = "<%= to("/admin/preview") %>";
|
|
var uploadUrl = "<%= to("/admin/uploads") %>";
|
|
var csrf = "<%= csrf_token %>";
|
|
var timer = null;
|
|
|
|
try { document.execCommand("defaultParagraphSeparator", false, "p"); } catch (e) {}
|
|
|
|
// --- Markdown <-> HTML -------------------------------------------------
|
|
|
|
function mdToHtml(md) {
|
|
var data = new URLSearchParams();
|
|
data.set("body", md);
|
|
data.set("_csrf", csrf);
|
|
return fetch(previewUrl, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: data.toString()
|
|
}).then(function (response) { return response.text(); });
|
|
}
|
|
|
|
function inlineToMd(node) {
|
|
var out = "";
|
|
node.childNodes.forEach(function (child) { out += inlineNode(child); });
|
|
return out;
|
|
}
|
|
|
|
function inlineNode(node) {
|
|
if (node.nodeType === 3) { return node.nodeValue.replace(/\s+/g, " "); }
|
|
if (node.nodeType !== 1) { return ""; }
|
|
var tag = node.tagName.toLowerCase();
|
|
var inner = inlineToMd(node);
|
|
if (tag === "strong" || tag === "b") { return "**" + inner.trim() + "**"; }
|
|
if (tag === "em" || tag === "i") { return "*" + inner.trim() + "*"; }
|
|
if (tag === "code") { return "`" + node.textContent + "`"; }
|
|
if (tag === "a") { return "[" + inner + "](" + (node.getAttribute("href") || "") + ")"; }
|
|
if (tag === "img") {
|
|
var title = node.getAttribute("title") || "";
|
|
var imgMd = " || "") + ")";
|
|
if (title) { imgMd = imgMd.replace(")", ' "' + title + '")'); }
|
|
return imgMd;
|
|
}
|
|
if (tag === "br") { return "\n"; }
|
|
return inner;
|
|
}
|
|
|
|
function listToMd(list, ordered) {
|
|
var index = 1;
|
|
var items = [];
|
|
list.childNodes.forEach(function (li) {
|
|
if (li.nodeType === 1 && li.tagName.toLowerCase() === "li") {
|
|
var marker = ordered ? (index++ + ". ") : "- ";
|
|
items.push(marker + inlineToMd(li).trim());
|
|
}
|
|
});
|
|
return items.join("\n");
|
|
}
|
|
|
|
function htmlToMd(root) {
|
|
var blocks = [];
|
|
root.childNodes.forEach(function (node) {
|
|
if (node.nodeType === 3) {
|
|
var text = node.nodeValue.trim();
|
|
if (text) { blocks.push(text); }
|
|
return;
|
|
}
|
|
if (node.nodeType !== 1) { return; }
|
|
var tag = node.tagName.toLowerCase();
|
|
if (/^h[1-6]$/.test(tag)) {
|
|
blocks.push("#".repeat(Number(tag.charAt(1))) + " " + inlineToMd(node).trim());
|
|
} else if (tag === "ul" || tag === "ol") {
|
|
blocks.push(listToMd(node, tag === "ol"));
|
|
} else if (tag === "blockquote") {
|
|
blocks.push(inlineToMd(node).trim().split("\n").map(function (line) {
|
|
return "> " + line;
|
|
}).join("\n"));
|
|
} else if (tag === "pre") {
|
|
blocks.push("```\n" + node.textContent.replace(/\n$/, "") + "\n```");
|
|
} else if (tag === "hr") {
|
|
blocks.push("---");
|
|
} else {
|
|
var content = inlineToMd(node).trim();
|
|
if (content) { blocks.push(content); }
|
|
}
|
|
});
|
|
return blocks.join("\n\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
}
|
|
|
|
// --- Live preview (markdown mode) — disabled by default ----------------
|
|
|
|
function renderPreview() {
|
|
mdToHtml(textarea.value).then(function (html) { preview.innerHTML = html; });
|
|
}
|
|
|
|
function schedule() {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(renderPreview, 250);
|
|
}
|
|
|
|
// --- Image upload ------------------------------------------------------
|
|
|
|
function uploadImage(file, onDone) {
|
|
var data = new FormData();
|
|
data.append("file", file);
|
|
data.append("_csrf", csrf);
|
|
fetch(uploadUrl, { method: "POST", body: data })
|
|
.then(function (response) { return response.json(); })
|
|
.then(function (json) { if (json.url) { onDone(json.url); } });
|
|
}
|
|
|
|
imageInput.addEventListener("change", function () {
|
|
var file = imageInput.files[0];
|
|
if (!file) { return; }
|
|
uploadImage(file, function (url) {
|
|
if (visualView.hidden) {
|
|
insertIntoTextarea("");
|
|
} else {
|
|
exec("insertImage", url);
|
|
}
|
|
});
|
|
imageInput.value = "";
|
|
});
|
|
|
|
// --- Cover image -------------------------------------------------------
|
|
|
|
function refreshCoverPreview() {
|
|
if (coverInput.value) {
|
|
coverPreview.src = coverInput.value;
|
|
coverPreview.hidden = false;
|
|
} else {
|
|
coverPreview.hidden = true;
|
|
coverPreview.removeAttribute("src");
|
|
}
|
|
}
|
|
|
|
document.getElementById("cover-upload").addEventListener("click", function () {
|
|
coverFile.click();
|
|
});
|
|
document.getElementById("cover-clear").addEventListener("click", function () {
|
|
coverInput.value = "";
|
|
refreshCoverPreview();
|
|
});
|
|
coverFile.addEventListener("change", function () {
|
|
var file = coverFile.files[0];
|
|
if (!file) { return; }
|
|
uploadImage(file, function (url) {
|
|
coverInput.value = url;
|
|
refreshCoverPreview();
|
|
});
|
|
coverFile.value = "";
|
|
});
|
|
coverInput.addEventListener("input", refreshCoverPreview);
|
|
refreshCoverPreview();
|
|
|
|
// --- Markdown toolbar --------------------------------------------------
|
|
|
|
function insertIntoTextarea(text) {
|
|
var start = textarea.selectionStart;
|
|
textarea.value = textarea.value.slice(0, start) + text + textarea.value.slice(textarea.selectionEnd);
|
|
textarea.focus();
|
|
textarea.selectionStart = textarea.selectionEnd = start + text.length;
|
|
schedule();
|
|
}
|
|
|
|
function wrap(before, after) {
|
|
var start = textarea.selectionStart;
|
|
var end = textarea.selectionEnd;
|
|
var selected = textarea.value.slice(start, end);
|
|
textarea.value = textarea.value.slice(0, start) + before + selected + after +
|
|
textarea.value.slice(end);
|
|
textarea.focus();
|
|
textarea.selectionStart = start + before.length;
|
|
textarea.selectionEnd = end + before.length;
|
|
schedule();
|
|
}
|
|
|
|
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.focus();
|
|
schedule();
|
|
}
|
|
|
|
var mdActions = {
|
|
bold: function () { wrap("**", "**"); },
|
|
italic: function () { wrap("*", "*"); },
|
|
code: function () { wrap("`", "`"); },
|
|
link: function () { wrap("[", "](https://)"); },
|
|
h2: function () { linePrefix("## "); },
|
|
ul: function () { linePrefix("- "); },
|
|
quote: function () { linePrefix("> "); },
|
|
image: function () { imageInput.click(); }
|
|
};
|
|
|
|
// --- Visual toolbar ----------------------------------------------------
|
|
|
|
function exec(command, value) {
|
|
document.execCommand(command, false, value || null);
|
|
wysiwyg.focus();
|
|
updateToolbarState();
|
|
}
|
|
|
|
function wrapInline(tagName) {
|
|
var selection = window.getSelection();
|
|
if (!selection.rangeCount) { return; }
|
|
var element = document.createElement(tagName);
|
|
try { selection.getRangeAt(0).surroundContents(element); } catch (e) { /* spans blocks */ }
|
|
wysiwyg.focus();
|
|
updateToolbarState();
|
|
}
|
|
|
|
var richActions = {
|
|
bold: function () { exec("bold"); },
|
|
italic: function () { exec("italic"); },
|
|
h2: function () { exec("formatBlock", "H2"); },
|
|
h3: function () { exec("formatBlock", "H3"); },
|
|
quote: function () { exec("formatBlock", "BLOCKQUOTE"); },
|
|
clear: function () { exec("formatBlock", "P"); },
|
|
ul: function () { exec("insertUnorderedList"); },
|
|
ol: function () { exec("insertOrderedList"); },
|
|
code: function () { wrapInline("code"); },
|
|
image: function () { imageInput.click(); },
|
|
link: function () { var url = prompt("Link URL", "https://"); if (url) { exec("createLink", url); } }
|
|
};
|
|
|
|
// --- Toolbar active state ----------------------------------------------
|
|
|
|
function queryState(command) {
|
|
try { return document.queryCommandState(command); } catch (e) { return false; }
|
|
}
|
|
|
|
function currentBlock() {
|
|
try { return (document.queryCommandValue("formatBlock") || "").toLowerCase().replace(/[<>]/g, ""); }
|
|
catch (e) { return ""; }
|
|
}
|
|
|
|
function selectionInEditor() {
|
|
var node = window.getSelection().anchorNode;
|
|
while (node) { if (node === wysiwyg) { return true; } node = node.parentNode; }
|
|
return false;
|
|
}
|
|
|
|
function isInside(tagName) {
|
|
var node = window.getSelection().anchorNode;
|
|
while (node && node !== wysiwyg) {
|
|
if (node.nodeType === 1 && node.tagName.toLowerCase() === tagName) { return true; }
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function updateToolbarState() {
|
|
if (visualView.hidden) { return; }
|
|
var active = {};
|
|
if (selectionInEditor()) {
|
|
var block = currentBlock();
|
|
active = {
|
|
bold: queryState("bold"),
|
|
italic: queryState("italic"),
|
|
ul: queryState("insertUnorderedList"),
|
|
ol: queryState("insertOrderedList"),
|
|
h2: block === "h2",
|
|
h3: block === "h3",
|
|
quote: block === "blockquote",
|
|
link: isInside("a"),
|
|
code: isInside("code")
|
|
};
|
|
}
|
|
document.querySelectorAll("[data-rich]").forEach(function (button) {
|
|
button.classList.toggle("active", !!active[button.dataset.rich]);
|
|
});
|
|
}
|
|
|
|
// --- Mode switching ----------------------------------------------------
|
|
|
|
function setActiveTab(name) {
|
|
document.querySelectorAll(".editor-tabs button").forEach(function (button) {
|
|
button.classList.toggle("active", button.dataset.tab === name);
|
|
});
|
|
}
|
|
|
|
function showVisual() {
|
|
mdToHtml(textarea.value).then(function (html) { wysiwyg.innerHTML = html; });
|
|
visualView.hidden = false;
|
|
markdownView.hidden = true;
|
|
setActiveTab("visual");
|
|
}
|
|
|
|
function showMarkdown() {
|
|
textarea.value = htmlToMd(wysiwyg);
|
|
renderPreview();
|
|
visualView.hidden = true;
|
|
markdownView.hidden = false;
|
|
setActiveTab("markdown");
|
|
}
|
|
|
|
// --- Keyboard shortcuts ------------------------------------------------
|
|
|
|
function shortcut(actions) {
|
|
return function (event) {
|
|
if (!(event.ctrlKey || event.metaKey)) { return; }
|
|
var key = event.key.toLowerCase();
|
|
if (key === "b") { event.preventDefault(); actions.bold(); }
|
|
else if (key === "i") { event.preventDefault(); actions.italic(); }
|
|
else if (key === "k") { event.preventDefault(); actions.link(); }
|
|
};
|
|
}
|
|
|
|
// --- Wiring ------------------------------------------------------------
|
|
|
|
document.querySelectorAll("[data-md]").forEach(function (button) {
|
|
button.addEventListener("click", function () { mdActions[button.dataset.md](); });
|
|
});
|
|
document.querySelectorAll("[data-rich]").forEach(function (button) {
|
|
button.addEventListener("click", function () { richActions[button.dataset.rich](); });
|
|
});
|
|
document.querySelectorAll(".editor-tabs button").forEach(function (button) {
|
|
button.addEventListener("click", function () {
|
|
if (button.dataset.tab === "visual") { showVisual(); } else { showMarkdown(); }
|
|
});
|
|
});
|
|
|
|
wysiwyg.addEventListener("paste", function (event) {
|
|
event.preventDefault();
|
|
var text = (event.clipboardData || window.clipboardData).getData("text/plain");
|
|
document.execCommand("insertText", false, text);
|
|
});
|
|
wysiwyg.addEventListener("keydown", shortcut(richActions));
|
|
wysiwyg.addEventListener("keyup", updateToolbarState);
|
|
wysiwyg.addEventListener("mouseup", updateToolbarState);
|
|
document.addEventListener("selectionchange", updateToolbarState);
|
|
|
|
textarea.addEventListener("input", schedule);
|
|
textarea.addEventListener("keydown", shortcut(mdActions));
|
|
|
|
form.addEventListener("submit", function () {
|
|
if (!visualView.hidden) { textarea.value = htmlToMd(wysiwyg); }
|
|
});
|
|
|
|
// --- Preview toggle (disabled by default) ------------------------------
|
|
|
|
var editorGrid = document.getElementById("editor-grid");
|
|
var previewPane = document.getElementById("preview-pane");
|
|
var togglePreviewBtn = document.getElementById("toggle-preview");
|
|
|
|
function applyPreviewPreference() {
|
|
var on = false;
|
|
try { on = localStorage.getItem("volumen.preview") === "on"; } catch (e) {}
|
|
if (on) {
|
|
editorGrid.classList.add("with-preview");
|
|
previewPane.hidden = false;
|
|
togglePreviewBtn.classList.add("active");
|
|
renderPreview();
|
|
} else {
|
|
editorGrid.classList.remove("with-preview");
|
|
previewPane.hidden = true;
|
|
togglePreviewBtn.classList.remove("active");
|
|
}
|
|
}
|
|
|
|
togglePreviewBtn.addEventListener("click", function () {
|
|
var turningOn = !editorGrid.classList.contains("with-preview");
|
|
try { localStorage.setItem("volumen.preview", turningOn ? "on" : "off"); } catch (e) {}
|
|
applyPreviewPreference();
|
|
});
|
|
|
|
// Start in Markdown mode (primary), preview OFF by default.
|
|
applyPreviewPreference();
|
|
})();
|
|
</script>
|