diff --git a/publish/editor.js b/publish/editor.js index 7049c7f..6e81fa2 100644 --- a/publish/editor.js +++ b/publish/editor.js @@ -13,6 +13,9 @@ * padding-left steps of 30px. The server cleans the story with HTML * Purifier in any case. * + * The source code button swaps the editor for the plain textarea, so that + * the HTML can be edited directly, and swaps it back when pressed again. + * * Usage: XxxEditor.attach(textarea) returns an object with getContent(), * setContent(html) and focus(). */ @@ -347,6 +350,7 @@ var XxxEditor = (function () { var redoStack = []; var lastInput = { time: 0, type: "" }; var savedRange = null; + var sourceMode = false; // Paragraphs are the default block, as in TinyMCE try { document.execCommand("defaultParagraphSeparator", false, "p"); } catch (e) { } @@ -354,6 +358,8 @@ var XxxEditor = (function () { /* ---------- content ---------- */ function sync() { + // In source mode the textarea itself holds the story + if (sourceMode) return; var html = serialize(box); if (textarea.value !== html) { textarea.value = html; @@ -406,6 +412,7 @@ var XxxEditor = (function () { } function setContent(html) { + if (sourceMode) showEditor(); box.textContent = ""; box.appendChild(parse(html)); // " " in an empty paragraph becomes a
that holds the caret @@ -771,24 +778,50 @@ var XxxEditor = (function () { }); body.appendChild(table); body.appendChild(preview); - dialog = openDialog("Special character", body, [{ text: "Close" }]); + dialog = openDialog("Special character", body, [{ text: "Close" }], null, true); } /* ---------- source code ---------- */ - function codeDialog() { - var source = el("textarea", "xed-source"); - source.value = serialize(box); - source.spellcheck = false; - openDialog("Source code", source, [ - { text: "Ok", primary: true, action: function () { - remember(); - setContent(source.value); - box.focus(); - changed(); - } }, - { text: "Cancel" } - ], source, "xed-window-code"); + // Show the plain textarea in place of the editor, with the HTML in it + function showSource() { + sync(); + sourceMode = true; + box.hidden = true; + textarea.hidden = false; + textarea.style.display = ""; + Object.keys(buttons).forEach(function (command) { + if (command !== "code") buttons[command].disabled = true; + }); + buttons.code.classList.add("xed-active"); + buttons.code.setAttribute("aria-pressed", "true"); + textarea.focus(); + } + + // Back to the editor, with the HTML from the textarea (cleaned) + function showEditor() { + sourceMode = false; + textarea.hidden = true; + textarea.style.display = "none"; + box.hidden = false; + Object.keys(buttons).forEach(function (command) { + buttons[command].disabled = false; + }); + buttons.code.classList.remove("xed-active"); + buttons.code.setAttribute("aria-pressed", "false"); + } + + function toggleSource() { + if (!sourceMode) { + showSource(); + return; + } + var html = textarea.value; + remember(); + showEditor(); + setContent(html); + box.focus(); + changed(); } function restoreSelection() { @@ -799,10 +832,10 @@ var XxxEditor = (function () { /* ---------- dialogs ---------- */ - // A modal window in the style of the TinyMCE skin - function openDialog(title, content, actions, focus, extraClass) { - var block = el("div", "xed-modal-block"); - var win = el("div", "xed-window" + (extraClass ? " " + extraClass : "")); + // A modal window. With *light*, a click outside the window closes it. + function openDialog(title, content, actions, focus, light) { + var block = el("div", "xed-modal-block" + (light ? " xed-modal-light" : "")); + var win = el("div", "xed-window"); win.setAttribute("role", "dialog"); win.setAttribute("aria-modal", "true"); var head = el("div", "xed-window-head"); @@ -842,6 +875,7 @@ var XxxEditor = (function () { foot.appendChild(button); }); close.addEventListener("click", cancel); + if (light) block.addEventListener("click", cancel); function onKey(event) { if (event.key === "Escape") { event.preventDefault(); @@ -881,7 +915,7 @@ var XxxEditor = (function () { function run(command) { if (command === "link") return linkDialog(); if (command === "charmap") return charmapDialog(); - if (command === "code") return codeDialog(); + if (command === "code") return toggleSource(); box.focus(); if (!currentRange()) { // No caret yet: start at the end @@ -930,6 +964,7 @@ var XxxEditor = (function () { function refresh() { var range = document.activeElement === box ? currentRange() : null; Object.keys(buttons).forEach(function (command) { + if (command === "code") return; var on = !!range && isActive(command, range); buttons[command].classList.toggle("xed-active", on); if (buttons[command].hasAttribute("aria-pressed")) buttons[command].setAttribute("aria-pressed", on ? "true" : "false"); @@ -949,7 +984,7 @@ var XxxEditor = (function () { button.type = "button"; button.title = spec.title + (spec.key ? " (Ctrl+" + spec.key.toUpperCase() + ")" : ""); button.setAttribute("aria-label", spec.title); - if (/^(bold|italic|align|blockquote)/.test(spec.command)) button.setAttribute("aria-pressed", "false"); + if (/^(bold|italic|align|blockquote|code)/.test(spec.command)) button.setAttribute("aria-pressed", "false"); button.appendChild(icon(spec.command)); // Keep the selection in the editor when the button is pressed button.addEventListener("mousedown", function (event) { event.preventDefault(); }); @@ -1051,13 +1086,14 @@ var XxxEditor = (function () { // The site style sets display on #story, which beats the hidden attribute textarea.hidden = true; textarea.style.display = "none"; + textarea.classList.add("xed-source"); setContent(textarea.value); if (textarea.form) textarea.form.addEventListener("submit", sync); return { - getContent: function () { return serialize(box); }, + getContent: function () { return sourceMode ? textarea.value : serialize(box); }, setContent: function (html) { setContent(html); }, - focus: function () { box.focus(); } + focus: function () { (sourceMode ? textarea : box).focus(); } }; }