diff --git a/OliverBooth/Controllers/Api/v1/BlogPostController.cs b/OliverBooth/Controllers/Api/v1/BlogPostController.cs deleted file mode 100644 index 75066c9..0000000 --- a/OliverBooth/Controllers/Api/v1/BlogPostController.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Asp.Versioning; -using Microsoft.AspNetCore.Mvc; -using OliverBooth.Data.Blog; -using OliverBooth.Data.Web; -using OliverBooth.Services; - -namespace OliverBooth.Controllers.Api.v1; - -[ApiController] -[Route("api/v{version:apiVersion}/post")] -[ApiVersion(1)] -[Produces("application/json")] -public sealed class BlogPostController : ControllerBase -{ - private readonly ILogger _logger; - private readonly ISessionService _sessionService; - private readonly IBlogPostService _blogPostService; - - public BlogPostController(ILogger logger, - ISessionService sessionService, - IBlogPostService blogPostService) - { - _logger = logger; - _sessionService = sessionService; - _blogPostService = blogPostService; - } - - [HttpPatch("{id:guid}")] - public async Task OnPatch([FromRoute] Guid id) - { - if (!_sessionService.TryGetCurrentUser(Request, Response, out IUser? user)) - { - Response.StatusCode = 401; - return new JsonResult(new { status = 401, message = "Unauthorized" }); - } - - if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) - { - Response.StatusCode = 404; - return new JsonResult(new { status = 404, message = "Not Found" }); - } - - var body = await JsonSerializer.DeserializeAsync>(Request.Body); - if (body is null) - { - Response.StatusCode = 400; - return new JsonResult(new { status = 400, message = "Bad Request" }); - } - - post.Body = body["content"]; - post.Title = body["title"]; - _blogPostService.UpdatePost(post); - - return new JsonResult(new { status = 200, message = "OK" }); - } -} diff --git a/OliverBooth/Pages/Admin/EditBlogPost.cshtml b/OliverBooth/Pages/Admin/EditBlogPost.cshtml index a559ede..42d9dd9 100644 --- a/OliverBooth/Pages/Admin/EditBlogPost.cshtml +++ b/OliverBooth/Pages/Admin/EditBlogPost.cshtml @@ -10,6 +10,8 @@ IBlogPost post = Model.BlogPost; } + +
@@ -21,11 +23,15 @@ - + - +
Post ID + +
Title + +
@@ -40,4 +46,4 @@
- + \ No newline at end of file diff --git a/OliverBooth/Pages/Components/MarkdownEditor.razor b/OliverBooth/Pages/Components/MarkdownEditor.razor new file mode 100644 index 0000000..59f7595 --- /dev/null +++ b/OliverBooth/Pages/Components/MarkdownEditor.razor @@ -0,0 +1,37 @@ +@using OliverBooth.Services +@using OliverBooth.Data.Blog +@implements IDisposable +@inject IBlogPostService BlogPostService +@inject IJSRuntime JsRuntime + +@code { + private DotNetObjectReference? _dotNetHelper; + + [JSInvokable] + public void Save(Guid id, string content) + { + if (!BlogPostService.TryGetPost(id, out IBlogPost? post)) + { + return; + } + + post.Body = content; + BlogPostService.UpdatePost(post); + } + + public void Dispose() + { + _dotNetHelper?.Dispose(); + } + + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _dotNetHelper = DotNetObjectReference.Create(this); + await JsRuntime.InvokeVoidAsync("Interop.setDotNetHelper", _dotNetHelper); + } + } + +} \ No newline at end of file diff --git a/OliverBooth/Pages/_ViewImports.cshtml b/OliverBooth/Pages/_ViewImports.cshtml index 47d742e..0f729c9 100644 --- a/OliverBooth/Pages/_ViewImports.cshtml +++ b/OliverBooth/Pages/_ViewImports.cshtml @@ -1,2 +1,4 @@ @namespace OliverBooth.Pages +@using Microsoft.AspNetCore.Components.Web +@using OliverBooth.Pages.Components @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/src/scss/admin.scss b/src/scss/admin.scss index db97261..89a0a92 100644 --- a/src/scss/admin.scss +++ b/src/scss/admin.scss @@ -74,7 +74,6 @@ textarea { border: 0; width: 100%; height: 100%; - position: relative; margin: 0; } @@ -95,7 +94,6 @@ textarea { background: transparent; caret-color: #FFFFFF; resize: none; - overflow-wrap: normal; overflow-x: scroll; white-space: pre; @@ -122,7 +120,7 @@ textarea { .highlighting-container .code-toolbar { height: 100%; - + .toolbar { display: none; } diff --git a/src/ts/admin/AdminUI.ts b/src/ts/admin/AdminUI.ts deleted file mode 100644 index 33f799d..0000000 --- a/src/ts/admin/AdminUI.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 fbcc96e..4156d4e 100644 --- a/src/ts/admin/EditBlogPost.ts +++ b/src/ts/admin/EditBlogPost.ts @@ -1,8 +1,10 @@ import BlogPost from "../app/BlogPost"; +import UI from "./MarkdownEditor/UI" import API from "../app/API"; -import UI from "../app/UI"; -import AdminUI from "./AdminUI"; import "./TabSupport" +import Interop from "./Interop"; +import MarkdownEditor from "./MarkdownEditor/MarkdownEditor"; +import SaveButtonMode from "./MarkdownEditor/SaveButtonMode"; (() => { getCurrentBlogPost().then(post => { @@ -10,48 +12,24 @@ import "./TabSupport" return; } - AdminUI.init(); + UI.init(); + UI.addSaveButtonListener(savePost); - 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(); - }); - - document.addEventListener("keydown", async (e: KeyboardEvent) => { - if (e.ctrlKey && e.key === "s") { - e.preventDefault(); - await savePost(); - preview.innerHTML = post.content; - UI.updateUI(preview); - // Prism.highlightAllUnder(preview); - } - }); + const editor = new MarkdownEditor(UI.markdownInput); + editor.addSaveListener(savePost); + editor.registerDefaultShortcuts(); + editor.registerEvents(); async function savePost(): Promise { - const saveButton = AdminUI.saveButton; - saveButton.classList.add("btn-primary"); - saveButton.classList.remove("btn-success"); + UI.setSaveButtonMode(SaveButtonMode.SAVING); + await Interop.invoke("Save", post.id, UI.markdownInput.value); + post = await API.getBlogPost(post.id); + UI.setSaveButtonMode(SaveButtonMode.SAVED); - saveButton.setAttribute("disabled", "disabled"); - saveButton.innerHTML = ' Saving ...'; - - post = await API.updatePost(post, {content: AdminUI.content.value, title: title.value}); - - saveButton.classList.add("btn-success"); - saveButton.classList.remove("btn-primary"); - saveButton.removeAttribute("disabled"); - saveButton.innerHTML = ' Saved'; - - setTimeout(() => { - saveButton.classList.add("btn-primary"); - saveButton.classList.remove("btn-success"); - saveButton.innerHTML = ' Save (Ctrl+S)'; - }, 2000); + setTimeout(() => UI.setSaveButtonMode(SaveButtonMode.NORMAL), 2000); } - AdminUI.updateEditView(); + UI.redraw(); }); async function getCurrentBlogPost(): Promise { diff --git a/src/ts/admin/HTMLTextAreaElement.d.ts b/src/ts/admin/HTMLTextAreaElement.d.ts new file mode 100644 index 0000000..2030c41 --- /dev/null +++ b/src/ts/admin/HTMLTextAreaElement.d.ts @@ -0,0 +1,3 @@ +declare interface HTMLTextAreaElement { + insertAt(text: string, position: number) : void; +} diff --git a/src/ts/admin/Interop.ts b/src/ts/admin/Interop.ts new file mode 100644 index 0000000..77b78ac --- /dev/null +++ b/src/ts/admin/Interop.ts @@ -0,0 +1,23 @@ +declare interface DotNetHelper { + invokeMethodAsync: (methodName: string, ...args: any) => Promise; +} + +class Interop { + private static _dotNetHelper: DotNetHelper; + + public static invoke(methodName: string, ...args: any): Promise { + return Interop.dotNetHelper.invokeMethodAsync(methodName, ...args); + } + + public static get dotNetHelper() { + return this._dotNetHelper; + } + + static setDotNetHelper(value: DotNetHelper) { + this._dotNetHelper = value; + } +} + +// @ts-ignore +window.Interop = Interop; +export default Interop; \ No newline at end of file diff --git a/src/ts/admin/MarkdownEditor/KeyboardShortcut.ts b/src/ts/admin/MarkdownEditor/KeyboardShortcut.ts new file mode 100644 index 0000000..cc9f4d2 --- /dev/null +++ b/src/ts/admin/MarkdownEditor/KeyboardShortcut.ts @@ -0,0 +1,42 @@ +/** + * Represents a keyboard modifier. + */ +export enum KeyboardModifier { + CTRL = 1, + SHIFT = 2, + ALT = 4 +} + +/** + * Represents a keyboard shortcut. + */ +export class KeyboardShortcut { + private readonly _key: string; + private readonly _modifier: KeyboardModifier; + + /** + * Initializes a new instance of the {@link KeyboardShortcut} class. + * @param key The key. + * @param modifier The modifier, if any. + */ + constructor(key: string, modifier?: KeyboardModifier) { + this._key = key; + this._modifier = modifier; + } + + /** + * Gets the key for this keyboard shortcut. + */ + public get key(): string { + return this._key; + } + + /** + * Gets the modifier for this keyboard shortcut. + */ + public get modifier(): KeyboardModifier { + return this._modifier; + } +} + +export default {KeyboardShortcut, KeyboardModifier}; \ No newline at end of file diff --git a/src/ts/admin/MarkdownEditor/MarkdownEditor.ts b/src/ts/admin/MarkdownEditor/MarkdownEditor.ts new file mode 100644 index 0000000..7a82ac7 --- /dev/null +++ b/src/ts/admin/MarkdownEditor/MarkdownEditor.ts @@ -0,0 +1,254 @@ +import "../Prototypes"; +import {KeyboardModifier, KeyboardShortcut} from "./KeyboardShortcut"; +import MarkdownFormatting from "./MarkdownFormatting"; +import UI from "./UI"; + +/** + * Represents a class which implements the Markdown editor and its controls. + */ +class MarkdownEditor { + private readonly _element: HTMLTextAreaElement; + private _nextSelection?: number[]; + private _saveListeners: Function[] = []; + + /** + * Initializes a new instance of the {@link MarkdownEditor} class. + * @param element The editor's {@link HTMLTextAreaElement}. + */ + constructor(element: HTMLTextAreaElement) { + this._element = element; + } + + /** + * Adds a callback that will be invoked when the save shortcut is executed. + * @param callback The callback to invoke. + */ + public addSaveListener(callback: Function) { + if (callback) { + this._saveListeners.push(callback); + } + } + + /** + * Applies Markdown formatting to the current selection. + * @param formatting The formatting to apply. + */ + public formatSelection(formatting: MarkdownFormatting): void { + if (!formatting) { + return; + } + + const el: HTMLTextAreaElement = this._element; + const selectionStart: number = el.selectionStart; + const selectionEnd: number = el.selectionEnd; + + const formatString = formatting.toString(); + const len = formatString.length; + + el.insertAt(formatString, selectionStart); + el.insertAt(formatString, selectionEnd + len); + + el.selectionStart = selectionStart + len; + el.selectionEnd = selectionEnd + len; + + UI.redraw(); + } + + /** + * Inserts a Markdown hyperlink at the current caret position. + */ + public insertLink(): void { + const el: HTMLTextAreaElement = this._element; + const selectionStart: number = el.selectionStart; + const selectionEnd: number = el.selectionEnd; + + if (selectionStart === selectionEnd) { + el.insertAt("[]()", selectionStart); + el.selectionStart = selectionStart + 1; + el.selectionEnd = selectionStart + 7; + + this._nextSelection = [2, 5]; + } else { + el.insertAt("[", selectionStart); + el.insertAt("]()", selectionEnd + 1); + el.selectionStart = selectionEnd + 3; + el.selectionEnd = el.selectionStart + 5; + } + + UI.redraw(); + } + + /** + * Pastes the current clipboard content into the editor. + */ + public async pasteClipboard(event: ClipboardEvent): Promise { + const files: FileList = event.clipboardData.files; + if (files.length) { + const file: File = files[0]; + const data: ArrayBuffer = await file.arrayBuffer(); + console.log(data); + } + + console.log(files); + } + + /** + * Registers all default keyboard shortcuts for the editor. + */ + public registerDefaultShortcuts() { + this.registerShortcut(new KeyboardShortcut('b', KeyboardModifier.CTRL), () => this.formatSelection(MarkdownFormatting.BOLD)); + this.registerShortcut(new KeyboardShortcut('i', KeyboardModifier.CTRL), () => this.formatSelection(MarkdownFormatting.ITALIC)); + this.registerShortcut(new KeyboardShortcut('k', KeyboardModifier.CTRL), () => this.insertLink()); + this.registerShortcut(new KeyboardShortcut('s', KeyboardModifier.CTRL), () => this.save()); + + this._element.addEventListener("paste", (ev: ClipboardEvent) => this.pasteClipboard(ev)); + } + + /** + * Registers a new keyboard shortcut. + * @param shortcut The keyboard shortcut for which to listen. + * @param action The action to invoke when the shortcut is executed. + */ + public registerShortcut(shortcut: KeyboardShortcut, action: Function): void { + if (!shortcut) throw new Error("No shortcut provided"); + if (!action) throw new Error("No callback function provided"); + + this._element.addEventListener("keydown", (ev: KeyboardEvent) => { + if ((shortcut.modifier & KeyboardModifier.CTRL) && !ev.ctrlKey) { + return; + } + + if ((shortcut.modifier & KeyboardModifier.SHIFT) && !ev.shiftKey) { + return; + } + + if ((shortcut.modifier & KeyboardModifier.ALT) && !ev.altKey) { + return; + } + + if (ev.key == shortcut.key) { + ev.preventDefault(); + action(); + } + }); + + console.log("Bound shortcut", shortcut, "to", action); + } + + public registerEvents() { + this.handleQuoteNewLine(); + this.handleTab(); + } + + /** + * Saves the content in the editor. + */ + public save() { + this._saveListeners.forEach(fn => fn()); + UI.redraw(); + } + + private handleTab() { + const el = this._element; + el.addEventListener("keydown", (ev: KeyboardEvent) => { + if (ev.key !== "Tab") { + return; + } + + ev.preventDefault(); + + if (this._nextSelection) { + el.selectionStart += this._nextSelection[0]; + el.selectionEnd = el.selectionEnd + this._nextSelection[1]; + return; + } + + let text = el.value; + let selStart = el.selectionStart; + let selEnd = el.selectionEnd; + + // selection? + if (selStart === selEnd) { + // These single character operations are undoable + if (ev.shiftKey) { + text = el.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 + text = el.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 (ev.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 + el.value = text.slice(0, selStart) + result + text.slice(selEnd); + el.selectionStart = selStart; + el.selectionEnd = selStart + lines.length; + + UI.redraw(); + } + }); + } + + private handleQuoteNewLine() { + const el = this._element; + el.addEventListener("keydown", (ev: KeyboardEvent) => { + if (ev.key !== "Enter") { + return; + } + + const selectionStart = el.selectionStart; + const selectionEnd = el.selectionEnd; + + let lineStart = selectionStart; + while (lineStart > 0 && el.value[lineStart - 1] !== '\n') { + lineStart--; + } + + const lineEnd = el.value.indexOf('\n', lineStart); + const line = el.value.slice(lineStart, lineEnd); + + if (line.startsWith("> ")) { + ev.preventDefault(); + + el.insertAt("\n> ", lineEnd); + el.blur(); + el.focus(); + el.selectionEnd = selectionEnd + 3; + + UI.redraw(); + } + }); + } +} + +export default MarkdownEditor; \ No newline at end of file diff --git a/src/ts/admin/MarkdownEditor/MarkdownFormatting.ts b/src/ts/admin/MarkdownEditor/MarkdownFormatting.ts new file mode 100644 index 0000000..0435fe5 --- /dev/null +++ b/src/ts/admin/MarkdownEditor/MarkdownFormatting.ts @@ -0,0 +1,9 @@ +/** + * An enumeration of Markdown formatting strings. + */ +enum MarkdownFormatting { + BOLD = "**", + ITALIC = "*" +} + +export default MarkdownFormatting; \ No newline at end of file diff --git a/src/ts/admin/MarkdownEditor/Prism.d.ts b/src/ts/admin/MarkdownEditor/Prism.d.ts new file mode 100644 index 0000000..b3e9cc8 --- /dev/null +++ b/src/ts/admin/MarkdownEditor/Prism.d.ts @@ -0,0 +1,13 @@ +declare interface PrismLanguage { +} + +declare interface Prism { + languages: { + markdown: PrismLanguage + }; + + highlight(markdown: string, language: PrismLanguage): string; + highlightAllUnder(element: HTMLElement): void; +} + +declare const Prism: Prism; \ No newline at end of file diff --git a/src/ts/admin/MarkdownEditor/SaveButtonMode.ts b/src/ts/admin/MarkdownEditor/SaveButtonMode.ts new file mode 100644 index 0000000..62b7f15 --- /dev/null +++ b/src/ts/admin/MarkdownEditor/SaveButtonMode.ts @@ -0,0 +1,28 @@ +/** + * An enumeration of states that the save button can be in. + */ +enum SaveButtonMode { + /** + * The save button should give feedback that the save process failed. + */ + FAILURE, + + /** + * The save button should draw as normal. + */ + NORMAL, + + /** + * The save button should give feedback that the save process is in progress. + */ + SAVING, + + /** + * The save button should give feedback that the save process completed. + */ + SAVED, + + _UNUSED +} + +export default SaveButtonMode; \ No newline at end of file diff --git a/src/ts/admin/MarkdownEditor/UI.ts b/src/ts/admin/MarkdownEditor/UI.ts new file mode 100644 index 0000000..02beb01 --- /dev/null +++ b/src/ts/admin/MarkdownEditor/UI.ts @@ -0,0 +1,107 @@ +import SaveButtonMode from "./SaveButtonMode"; + +/** + * Represents a class which interacts with the editor DOM. + */ +class UI { + private static readonly SB_ColorClasses = ["danger", "primary", "primary", "success"]; + private static readonly SB_IconClasses = ["exclamation", "floppy-disk", "spinner fa-spin", "circle-check"]; + private static readonly SB_Text = ["Error Saving", "Save (Ctrl+S)", "Saving ...", "Saved"]; + + private static _highlightingContent: HTMLElement; + private static _highlighting: HTMLElement; + private static _content: HTMLTextAreaElement; + private static _saveButton: HTMLButtonElement; + + private static _saveCallbacks: Function[] = []; + + /** + * Returns the {@link HTMLTextAreaElement} where the Markdown is inputted by the user. + */ + public static get markdownInput(): HTMLTextAreaElement { + return UI._content; + } + + /** + * Adds a callback that will be invoked when the save button is pressed. + * @param callback The callback to invoke. + */ + public static addSaveButtonListener(callback: Function) { + if (!callback) { + return; + } + + this._saveCallbacks.push(callback); + } + + /** + * Initializes the editor's user interface. + */ + public static init() { + const content = UI._content; + UI._highlightingContent = document.getElementById("highlighting-content"); + UI._highlighting = document.getElementById("highlighting"); + content.addEventListener("input", () => UI.redraw()); + content.addEventListener("scroll", () => UI.syncEditorScroll()); + + UI._saveButton.addEventListener("click", (_: MouseEvent) => { + UI._saveCallbacks.forEach(fn => fn()); + }); + } + + /** + * Redraws the Markdown editor UI. + */ + public static redraw() { + UI._highlightingContent.innerHTML = Prism.highlight(UI._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); + }); + + UI.syncEditorScroll(); + } + + /** + * Sets the display mode of the save button. + * @param mode The display mode. + */ + public static setSaveButtonMode(mode: SaveButtonMode) { + if (mode >= SaveButtonMode._UNUSED) { + throw new Error("Invalid display mode"); + } + + UI._saveButton.innerHTML = UI.SB_Text[mode]; + UI.SB_ColorClasses.concat(UI.SB_IconClasses).forEach(c => UI._saveButton.classList.remove(c)); + UI._saveButton.classList.add(UI.SB_ColorClasses[mode]); + UI._saveButton.classList.add(UI.SB_IconClasses[mode]); + + if (mode === SaveButtonMode.NORMAL) { + UI._saveButton.removeAttribute("disabled"); + } else { + UI._saveButton.setAttribute("disabled", "disabled"); + } + } + + /** + * Synchronises the syntax highlighting element's scroll value with that of the input element. + */ + public static syncEditorScroll() { + UI._highlighting.scrollTop = UI._content.scrollTop; + UI._highlighting.scrollLeft = UI._content.scrollLeft; + } +} + +export default UI; diff --git a/src/ts/admin/Prototypes.ts b/src/ts/admin/Prototypes.ts new file mode 100644 index 0000000..a1c235c --- /dev/null +++ b/src/ts/admin/Prototypes.ts @@ -0,0 +1,3 @@ +HTMLTextAreaElement.prototype.insertAt = function (text: string, index: number) { + this.value = this.value.slice(0, index) + text + this.value.slice(index); +}; \ No newline at end of file diff --git a/src/ts/admin/TabSupport.ts b/src/ts/admin/TabSupport.ts index 250392e..bab813d 100644 --- a/src/ts/admin/TabSupport.ts +++ b/src/ts/admin/TabSupport.ts @@ -1,5 +1,5 @@ -import AdminUI from "./AdminUI"; -import adminUI from "./AdminUI"; +import "./Prototypes" +import UI from "./MarkdownEditor/UI"; (() => { const textareas = document.querySelectorAll("textarea.tab-support"); @@ -19,21 +19,14 @@ import adminUI from "./AdminUI"; 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)}`; @@ -42,70 +35,12 @@ import adminUI from "./AdminUI"; textarea.blur(); textarea.focus(); textarea.selectionEnd = selEnd + indentStr.length + 1; // +1 for \n - AdminUI.updateEditView(); + UI.redraw(); 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; }); }); diff --git a/src/ts/admin/admin.ts b/src/ts/admin/admin.ts index ce471a6..53cc0f7 100644 --- a/src/ts/admin/admin.ts +++ b/src/ts/admin/admin.ts @@ -1 +1 @@ -import "./EditBlogPost" \ No newline at end of file +import "./EditBlogPost"