Edit the story source in the textarea.

Also close the special character window on a click outside it.
This commit is contained in:
tim
2026-09-26 00:10:14 +02:00
parent 6c745ed6a9
commit babe5361e7
+55 -19
View File
@@ -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));
// "&nbsp;" in an empty paragraph becomes a <br> 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 () {
// 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();
setContent(source.value);
showEditor();
setContent(html);
box.focus();
changed();
} },
{ text: "Cancel" }
], source, "xed-window-code");
}
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(); }
};
}