feat: add support for maintaining indentation on newline
This commit is contained in:
parent
5b236da2e3
commit
71b1ff32c4
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="d-flex flex-row flex-fill">
|
<div class="d-flex flex-row flex-fill">
|
||||||
<div class="flex-fill mb-0 highlighting-container" style="border-right: 2px dashed #FFFFFF;">
|
<div class="flex-fill mb-0 highlighting-container" style="border-right: 2px dashed #FFFFFF;">
|
||||||
<textarea id="content" spellcheck="false">@post.Body</textarea>
|
<textarea id="content" class="tab-support" spellcheck="false">@post.Body</textarea>
|
||||||
<pre id="highlighting" aria-hidden="true"><code id="highlighting-content" class="language-markdown">@post.Body</code></pre>
|
<pre id="highlighting" aria-hidden="true"><code id="highlighting-content" class="language-markdown">@post.Body</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-fill mb-0" style="overflow-y: scroll; background: #1E1E1E; max-height: calc(100vh - 35px)">
|
<div class="flex-fill mb-0" style="overflow-y: scroll; background: #1E1E1E; max-height: calc(100vh - 35px)">
|
||||||
|
62
src/ts/admin/AdminUI.ts
Normal file
62
src/ts/admin/AdminUI.ts
Normal file
@ -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 = `<code class="${span.className} language-${language}" style="padding:0;">${span.innerHTML}</code>`;
|
||||||
|
Prism.highlightAllUnder(languageSpan.parentElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
AdminUI.syncEditorScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
static syncEditorScroll() {
|
||||||
|
AdminUI.highlighting.scrollTop = AdminUI._content.scrollTop;
|
||||||
|
AdminUI.highlighting.scrollLeft = AdminUI._content.scrollLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminUI;
|
@ -1,8 +1,8 @@
|
|||||||
import BlogPost from "../app/BlogPost";
|
import BlogPost from "../app/BlogPost";
|
||||||
import API from "../app/API";
|
import API from "../app/API";
|
||||||
import UI from "../app/UI";
|
import UI from "../app/UI";
|
||||||
|
import AdminUI from "./AdminUI";
|
||||||
declare const Prism: any;
|
import "./TabSupport"
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
getCurrentBlogPost().then(post => {
|
getCurrentBlogPost().then(post => {
|
||||||
@ -10,14 +10,12 @@ declare const Prism: any;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveButton = document.getElementById("save-button") as HTMLButtonElement;
|
AdminUI.init();
|
||||||
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");
|
|
||||||
|
|
||||||
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();
|
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<void> {
|
async function savePost(): Promise<void> {
|
||||||
|
const saveButton = AdminUI.saveButton;
|
||||||
saveButton.classList.add("btn-primary");
|
saveButton.classList.add("btn-primary");
|
||||||
saveButton.classList.remove("btn-success");
|
saveButton.classList.remove("btn-success");
|
||||||
|
|
||||||
saveButton.setAttribute("disabled", "disabled");
|
saveButton.setAttribute("disabled", "disabled");
|
||||||
saveButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin fa-fw"></i> Saving ...';
|
saveButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin fa-fw"></i> 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.add("btn-success");
|
||||||
saveButton.classList.remove("btn-primary");
|
saveButton.classList.remove("btn-primary");
|
||||||
@ -66,33 +51,7 @@ declare const Prism: any;
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEditView();
|
AdminUI.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 = `<code class="${span.className} language-${language}" style="padding:0;">${span.innerHTML}</code>`;
|
|
||||||
Prism.highlightAllUnder(highlightingContent);
|
|
||||||
});
|
|
||||||
syncEditorScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncEditorScroll() {
|
|
||||||
highlighting.scrollTop = content.scrollTop;
|
|
||||||
highlighting.scrollLeft = content.scrollLeft;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getCurrentBlogPost(): Promise<BlogPost> {
|
async function getCurrentBlogPost(): Promise<BlogPost> {
|
||||||
|
112
src/ts/admin/TabSupport.ts
Normal file
112
src/ts/admin/TabSupport.ts
Normal file
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
Loading…
Reference in New Issue
Block a user