From 71b1ff32c43609355bdbc11a053e4725ee0e8a2d Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Tue, 27 Feb 2024 16:08:55 +0000 Subject: [PATCH] feat: add support for maintaining indentation on newline --- OliverBooth/Pages/Admin/EditBlogPost.cshtml | 2 +- src/ts/admin/AdminUI.ts | 62 +++++++++++ src/ts/admin/EditBlogPost.ts | 61 ++--------- src/ts/admin/TabSupport.ts | 112 ++++++++++++++++++++ 4 files changed, 185 insertions(+), 52 deletions(-) create mode 100644 src/ts/admin/AdminUI.ts create mode 100644 src/ts/admin/TabSupport.ts diff --git a/OliverBooth/Pages/Admin/EditBlogPost.cshtml b/OliverBooth/Pages/Admin/EditBlogPost.cshtml index b3ddbd4..a559ede 100644 --- a/OliverBooth/Pages/Admin/EditBlogPost.cshtml +++ b/OliverBooth/Pages/Admin/EditBlogPost.cshtml @@ -31,7 +31,7 @@
- +
diff --git a/src/ts/admin/AdminUI.ts b/src/ts/admin/AdminUI.ts new file mode 100644 index 0000000..33f799d --- /dev/null +++ b/src/ts/admin/AdminUI.ts @@ -0,0 +1,62 @@ +import adminUI from "./AdminUI"; + +declare const Prism: any; +class AdminUI { + static highlightingContent: HTMLElement; + static highlighting: HTMLElement; + static _content: HTMLTextAreaElement; + static _saveButton: HTMLButtonElement; + + static init() { + const content = AdminUI.content; + AdminUI.highlightingContent = document.getElementById("highlighting-content"); + AdminUI.highlighting = document.getElementById("highlighting"); + content.addEventListener("input", () => AdminUI.updateEditView()); + content.addEventListener("scroll", () => AdminUI.syncEditorScroll()); + } + + public static get content() { + if (!AdminUI._content) { + AdminUI._content = document.getElementById("content") as HTMLTextAreaElement; + } + + return AdminUI._content; + } + + public static get saveButton() { + if (!AdminUI._saveButton) { + AdminUI._saveButton = document.getElementById("save-button") as HTMLButtonElement; + } + + return AdminUI._saveButton; + } + + public static updateEditView() { + AdminUI.highlightingContent.innerHTML = Prism.highlight(AdminUI.content.value, Prism.languages.markdown); + + document.querySelectorAll("#highlighting-content span.token.code").forEach(el => { + const languageSpan = el.querySelector(".code-language") as HTMLSpanElement; + if (!languageSpan) { + return; + } + + const language = languageSpan.innerText; + const span = el.querySelector(".code-block"); + if (!span) { + return; + } + + span.outerHTML = `${span.innerHTML}`; + Prism.highlightAllUnder(languageSpan.parentElement); + }); + + AdminUI.syncEditorScroll(); + } + + static syncEditorScroll() { + AdminUI.highlighting.scrollTop = AdminUI._content.scrollTop; + AdminUI.highlighting.scrollLeft = AdminUI._content.scrollLeft; + } +} + +export default AdminUI; \ No newline at end of file diff --git a/src/ts/admin/EditBlogPost.ts b/src/ts/admin/EditBlogPost.ts index 0de9fea..fbcc96e 100644 --- a/src/ts/admin/EditBlogPost.ts +++ b/src/ts/admin/EditBlogPost.ts @@ -1,8 +1,8 @@ import BlogPost from "../app/BlogPost"; import API from "../app/API"; import UI from "../app/UI"; - -declare const Prism: any; +import AdminUI from "./AdminUI"; +import "./TabSupport" (() => { getCurrentBlogPost().then(post => { @@ -10,14 +10,12 @@ declare const Prism: any; return; } - const saveButton = document.getElementById("save-button") as HTMLButtonElement; - const preview = document.getElementById("article-preview") as HTMLAnchorElement; - const content = document.getElementById("content") as HTMLTextAreaElement; - const title = document.getElementById("post-title") as HTMLInputElement; - const highlighting = document.getElementById("highlighting"); - const highlightingContent = document.getElementById("highlighting-content"); + AdminUI.init(); - saveButton.addEventListener("click", async (e: MouseEvent) => { + const preview = document.getElementById("article-preview") as HTMLAnchorElement; + const title = document.getElementById("post-title") as HTMLInputElement; + + AdminUI.saveButton.addEventListener("click", async (e: MouseEvent) => { await savePost(); }); @@ -31,28 +29,15 @@ declare const Prism: any; } }); - content.addEventListener("keydown", async (e: KeyboardEvent) => { - if (e.key === "Tab") { - e.preventDefault(); - - const start = content.selectionStart; - const end = content.selectionEnd; - const text = content.value; - content.value = `${text.slice(0, start)} ${text.slice(start, end)}`; - updateEditView(); - content.selectionStart = start + 4; - content.selectionEnd = end ? end + 4 : start + 4; - } - }); - async function savePost(): Promise { + const saveButton = AdminUI.saveButton; saveButton.classList.add("btn-primary"); saveButton.classList.remove("btn-success"); saveButton.setAttribute("disabled", "disabled"); saveButton.innerHTML = ' Saving ...'; - post = await API.updatePost(post, {content: content.value, title: title.value}); + post = await API.updatePost(post, {content: AdminUI.content.value, title: title.value}); saveButton.classList.add("btn-success"); saveButton.classList.remove("btn-primary"); @@ -66,33 +51,7 @@ declare const Prism: any; }, 2000); } - updateEditView(); - content.addEventListener("input", () => updateEditView()); - content.addEventListener("scroll", () => syncEditorScroll()); - function updateEditView() { - highlightingContent.innerHTML = Prism.highlight(content.value, Prism.languages.markdown); - document.querySelectorAll("#highlighting-content span.token.code").forEach(el => { - const languageSpan = el.querySelector(".code-language") as HTMLSpanElement; - if (!languageSpan) { - return; - } - - const language = languageSpan.innerText; - const span = el.querySelector(".code-block"); - if (!span) { - return; - } - - span.outerHTML = `${span.innerHTML}`; - Prism.highlightAllUnder(highlightingContent); - }); - syncEditorScroll(); - } - - function syncEditorScroll() { - highlighting.scrollTop = content.scrollTop; - highlighting.scrollLeft = content.scrollLeft; - } + AdminUI.updateEditView(); }); async function getCurrentBlogPost(): Promise { diff --git a/src/ts/admin/TabSupport.ts b/src/ts/admin/TabSupport.ts new file mode 100644 index 0000000..250392e --- /dev/null +++ b/src/ts/admin/TabSupport.ts @@ -0,0 +1,112 @@ +import AdminUI from "./AdminUI"; +import adminUI from "./AdminUI"; + +(() => { + const textareas = document.querySelectorAll("textarea.tab-support"); + textareas.forEach((textarea: HTMLTextAreaElement) => { + textarea.addEventListener("keydown", (e: KeyboardEvent) => { + let text: string; + + // Enter Key? + if (e.key === "Enter") { + const selStart = textarea.selectionStart; + const selEnd = textarea.selectionEnd; + let sel = selStart; + // selection? + if (sel == selEnd) { + // find start of the current line + let text = textarea.value; + while (sel > 0 && text[sel - 1] !== '\n') + sel--; + + console.log(`Line starts at index ${sel}`); + + const lineStart = sel; + while (text[sel] === ' ' || text[sel] === '\t') + sel++; + + console.log(`Identation ends at ${sel} (sel + ${sel - lineStart})`); + + if (sel > lineStart) { + const lineEnd = lineStart + text.indexOf('\n', lineStart); + console.log(`Line starts at index ${lineEnd}`); + e.preventDefault(); + + const indentStr = text.slice(lineStart, sel); + console.log(`Indent string is "${indentStr}"`); + + // insert carriage return and indented text + textarea.value = `${text.slice(0, selStart)}\n${indentStr}${text.slice(selStart)}`; + + // Scroll caret visible + textarea.blur(); + textarea.focus(); + textarea.selectionEnd = selEnd + indentStr.length + 1; // +1 for \n + AdminUI.updateEditView(); + return false; + } + } + } + + // Tab key? + if (e.key === "Tab") { + e.preventDefault(); + let selStart = textarea.selectionStart; + + // selection? + if (selStart == textarea.selectionEnd) { + // These single character operations are undoable + if (e.shiftKey) { + text = textarea.value; + if (selStart > 0 && (text[selStart - 1] === '\t' || text.slice(selStart - 4, selStart) === " ")) { + document.execCommand("delete"); + } + } else { + document.execCommand("insertText", false, " "); + } + } else { + // Block indent/unindent trashes undo stack. + // Select whole lines + let selEnd = textarea.selectionEnd; + text = textarea.value; + while (selStart > 0 && text[selStart - 1] !== '\n') + selStart--; + while (selEnd > 0 && text[selEnd - 1] !== '\n' && selEnd < text.length) + selEnd++; + + // Get selected text + let lines = text.slice(selStart, selEnd - selStart).split('\n'); + + // Insert tabs + for (let i = 0; i < lines.length; i++) { + // Don't indent last line if cursor at start of line + if (i == lines.length - 1 && lines[i].length == 0) + continue; + + // Tab or Shift+Tab? + if (e.shiftKey) { + if (lines[i].startsWith('\t')) + lines[i] = lines[i].slice(1); + else if (lines[i].startsWith(" ")) + lines[i] = lines[i].slice(4); + } else { + lines[i] = ` ${lines[i]}`; + } + } + + const result = lines.join('\n'); + + // Update the text area + textarea.value = text.slice(0, selStart) + result + text.slice(selEnd); + textarea.selectionStart = selStart; + textarea.selectionEnd = selStart + lines.length; + } + + AdminUI.updateEditView(); + return false; + } + + return true; + }); + }); +})(); \ No newline at end of file