feat: add syntax highlighting to post editor

This commit is contained in:
Oliver Booth 2024-02-26 17:43:58 +00:00
parent 6efbd749be
commit 593036a712
Signed by: oliverbooth
GPG Key ID: E60B570D1B7557B5
3 changed files with 134 additions and 7 deletions

View File

@ -12,12 +12,26 @@
<input type="hidden" data-blog-pid="@post.Id"> <input type="hidden" data-blog-pid="@post.Id">
<button id="save-button" class="btn btn-primary"><i class="fa-solid fa-floppy-disk fa-fw"></i> Save <span class="text-muted">(Ctrl+S)</span></button> <div style="margin-bottom: 20px;">
<a href="/blog/@post.Published.ToString(@"yyyy\/MM\/dd")/@post.Slug" target="_blank" class="btn btn-info"><i class="fa-solid fa-magnifying-glass"></i> Preview</a> <button id="save-button" class="btn btn-primary"><i class="fa-solid fa-floppy-disk fa-fw"></i> Save <span class="text-muted">(Ctrl+S)</span></button>
<a href="/blog/@post.Published.ToString(@"yyyy\/MM\/dd")/@post.Slug" target="_blank" class="btn btn-info"><i class="fa-solid fa-magnifying-glass"></i> Preview</a>
</div>
<table class="table">
<tr>
<th>Post ID</th>
<td><input class="form-control" type="text" value="@post.Id" disabled="disabled"></td>
</tr>
<tr>
<th>Title</th>
<td><input class="form-control" id="post-title" type="text" value="@post.Title"></td>
</tr>
</table>
<div class="row" style="margin-top: 20px;"> <div class="row" style="margin-top: 20px;">
<div class="col-md-6 col-sm-12"> <div id="editing-area" class="col-md-6 col-sm-12">
<textarea id="content" style="width: 100%; font-family: monospace; min-height: calc(100vh - 80px); max-height: 100%">@post.Body</textarea> <textarea id="content" spellcheck="false">@post.Body</textarea>
<pre id="highlighting" aria-hidden="true"><code id="highlighting-content" class="language-markdown">@post.Body</code></pre>
</div> </div>
<div class="col-md-6 col-sm-12" style="overflow-y: scroll; background: #1E1E1E"> <div class="col-md-6 col-sm-12" style="overflow-y: scroll; background: #1E1E1E">
<article id="article-preview" style="background: #333; max-width: 700px; margin: 20px auto; padding: 20px;"> <article id="article-preview" style="background: #333; max-width: 700px; margin: 20px auto; padding: 20px;">

View File

@ -41,3 +41,66 @@ pre {
code[class*="language-"] { code[class*="language-"] {
background: none !important; background: none !important;
} }
article {
*:last-child {
margin-bottom: 0;
}
}
div.alert {
*:last-child {
margin-bottom: 0;
}
}
#save-button {
transition: 0.4s;
width: 10em;
}
textarea {
font-family: monospace;
}
#editing-area {
.toolbar {
display: none;
}
}
#highlighting, #content {
margin: 10px;
padding: 10px;
border: 0;
width: calc(100% - 32px);
height: 500px;
position: relative;
}
#highlighting, #content, #highlighting * {
font-size: 12pt;
font-family: monospace;
line-height: 15pt;
}
#highlighting {
margin-top: -511px;
z-index: 0;
background: #1E1E1E;
}
#content {
z-index: 10;
color: transparent;
background: transparent;
caret-color: #FFFFFF;
resize: none;
overflow-wrap: normal;
overflow-x: scroll;
white-space: pre;
}
#highlighting-content, #highlighting-content code {
white-space: pre !important;
}

View File

@ -1,5 +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";
declare const Prism: any;
(() => { (() => {
getCurrentBlogPost().then(post => { getCurrentBlogPost().then(post => {
@ -7,9 +10,12 @@ import API from "../app/API";
return; return;
} }
const saveButton = document.getElementById("save-button"); const saveButton = document.getElementById("save-button") as HTMLButtonElement;
const preview = document.getElementById("article-preview"); const preview = document.getElementById("article-preview") as HTMLAnchorElement;
const content = document.getElementById("content") as HTMLTextAreaElement; 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) => { saveButton.addEventListener("click", async (e: MouseEvent) => {
await savePost(); await savePost();
@ -20,6 +26,22 @@ import API from "../app/API";
e.preventDefault(); e.preventDefault();
await savePost(); await savePost();
preview.innerHTML = post.content; preview.innerHTML = post.content;
UI.updateUI(preview);
// Prism.highlightAllUnder(preview);
}
});
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;
} }
}); });
@ -43,6 +65,34 @@ import API from "../app/API";
saveButton.innerHTML = '<i class="fa-solid fa-floppy-disk fa-fw"></i> Save <span class="text-muted">(Ctrl+S)</span>'; saveButton.innerHTML = '<i class="fa-solid fa-floppy-disk fa-fw"></i> Save <span class="text-muted">(Ctrl+S)</span>';
}, 2000); }, 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 = `<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> {