From d032672076c573b8522fa2a14176c101e5a0b612 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Mon, 19 Dec 2022 11:50:48 +0100 Subject: [PATCH] Eliminiate vertical jump with directives --- web/cm_plugins/clean.ts | 2 +- web/cm_plugins/directive.ts | 74 +++++++++++++++++++++++++----- web/cm_plugins/util.ts | 21 +++++++++ web/styles/theme.scss | 90 ++++++++++++++++++++----------------- 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/web/cm_plugins/clean.ts b/web/cm_plugins/clean.ts index e16208c..dfa853a 100644 --- a/web/cm_plugins/clean.ts +++ b/web/cm_plugins/clean.ts @@ -16,7 +16,7 @@ import { cleanCommandLinkPlugin } from "./command_link.ts"; export function cleanModePlugins(editor: Editor) { return [ linkPlugin(editor), - directivePlugin(), + directivePlugin(editor), blockquotePlugin(), admonitionPlugin(editor), hideMarksPlugin(), diff --git a/web/cm_plugins/directive.ts b/web/cm_plugins/directive.ts index 08181c4..ac37f16 100644 --- a/web/cm_plugins/directive.ts +++ b/web/cm_plugins/directive.ts @@ -1,13 +1,14 @@ -import { Decoration, EditorState, syntaxTree } from "../deps.ts"; import { - decoratorStateField, - invisibleDecoration, - isCursorInRange, -} from "./util.ts"; + directiveEndRegex, + directiveStartRegex, +} from "../../plug-api/lib/query.ts"; +import { Decoration, EditorState, syntaxTree } from "../deps.ts"; +import type { Editor } from "../editor.tsx"; +import { decoratorStateField, HtmlWidget, isCursorInRange } from "./util.ts"; // Does a few things: hides the directives when the cursor is not placed inside // Adds a class to the start and end of the directive when the cursor is placed inside -export function directivePlugin() { +export function directivePlugin(editor: Editor) { return decoratorStateField((state: EditorState) => { const widgets: any[] = []; @@ -28,10 +29,34 @@ export function directivePlugin() { Decoration.line({ class: "sb-directive-start" }).range(from), ); } else { - widgets.push(invisibleDecoration.range(from, to)); + const text = state.sliceDoc(from, to); + const match = directiveStartRegex.exec(text); + if (!match) { + console.error("Something went wrong with this directive"); + return; + } + const [fullMatch, directiveName] = match; widgets.push( - Decoration.line({ class: "sb-directive-start-outside" }).range( - state.doc.lineAt(to).from, + Decoration.widget({ + widget: new HtmlWidget( + `#${directiveName}`, + "sb-directive-placeholder", + (e) => { + e.stopPropagation(); + editor.editorView?.dispatch({ + selection: { + anchor: from + fullMatch.indexOf(directiveName), + }, + }); + }, + ), + }).range(from), + ); + widgets.push( + Decoration.line({ + class: "sb-directive-start sb-directive-start-outside", + }).range( + from, ), ); } @@ -45,10 +70,34 @@ export function directivePlugin() { Decoration.line({ class: "sb-directive-end" }).range(from), ); } else { - widgets.push(invisibleDecoration.range(from, to)); + const text = state.sliceDoc(from, to); + const match = directiveEndRegex.exec(text); + if (!match) { + console.error("Something went wrong with this directive"); + return; + } + const [fullMatch, directiveName] = match; widgets.push( - Decoration.line({ class: "sb-directive-end-outside" }).range( - state.doc.lineAt(from - 1).from, + Decoration.widget({ + widget: new HtmlWidget( + `/${directiveName}`, + "sb-directive-placeholder", + (e) => { + e.stopPropagation(); + editor.editorView?.dispatch({ + selection: { + anchor: from + fullMatch.indexOf(directiveName), + }, + }); + }, + ), + }).range(from), + ); + widgets.push( + Decoration.line({ + class: "sb-directive-end sb-directive-end-outside", + }).range( + from, ), ); } @@ -68,6 +117,7 @@ export function directivePlugin() { } pos += line.length + 1; } + return true; } }, }); diff --git a/web/cm_plugins/util.ts b/web/cm_plugins/util.ts index 073e647..37c6d54 100644 --- a/web/cm_plugins/util.ts +++ b/web/cm_plugins/util.ts @@ -40,6 +40,27 @@ export class LinkWidget extends WidgetType { } } +export class HtmlWidget extends WidgetType { + constructor( + readonly html: string, + readonly className?: string, + readonly onClick?: (e: MouseEvent) => void, + ) { + super(); + } + toDOM(): HTMLElement { + const el = document.createElement("span"); + if (this.className) { + el.className = this.className; + } + if (this.onClick) { + el.addEventListener("click", this.onClick); + } + el.innerHTML = this.html; + return el; + } +} + export function decoratorStateField( stateToDecoratorMapper: (state: EditorState) => DecorationSet, ) { diff --git a/web/styles/theme.scss b/web/styles/theme.scss index 9af71c5..869c14d 100644 --- a/web/styles/theme.scss +++ b/web/styles/theme.scss @@ -1,6 +1,8 @@ #sb-root { --highlight-color: #464cfc; - --directive-color: #0000000f; + --directive-border-color: #0000000f; + --directive-font-color: #5b5b5b; + --ui-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --editor-font: "iA-Mono", "Menlo"; font-family: var(--ui-font); @@ -33,13 +35,14 @@ font-family: var(--editor-font); } -.sb-notifications > div { +.sb-notifications>div { border: rgb(41, 41, 41) 1px solid; } .sb-notification-info { background-color: rgb(187, 221, 247); } + .sb-notification-error { background-color: rgb(255, 84, 84); } @@ -250,8 +253,9 @@ .sb-line-li .sb-meta { color: rgb(150, 150, 150); } + /* Then undo other meta */ -.sb-line-li .sb-meta ~ .sb-meta { +.sb-line-li .sb-meta~.sb-meta { color: #650007; } @@ -377,13 +381,15 @@ sb-admonition-warning .sb-admonition-icon { // Directives + .sb-directive-body { - border-left: 1px solid var(--directive-color); - border-right: 1px solid var(--directive-color); + border-left: 1px solid var(--directive-border-color); + border-right: 1px solid var(--directive-border-color); } -.cm-line.sb-directive-start, .cm-line.sb-directive-end { - color: #5b5b5b; +.cm-line.sb-directive-start, +.cm-line.sb-directive-end { + color: var(--directive-font-color); background-color: rgb(233, 233, 233, 50%); padding: 3px; } @@ -392,7 +398,7 @@ sb-admonition-warning .sb-admonition-icon { border-top-left-radius: 10px; border-top-right-radius: 10px; border-style: solid; - border-color: var(--directive-color); + border-color: var(--directive-border-color); border-width: 1px 1px 0 1px; } @@ -400,37 +406,36 @@ sb-admonition-warning .sb-admonition-icon { border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; border-style: solid; - border-color: var(--directive-color); + border-color: var(--directive-border-color); border-width: 0 1px 1px 1px; } .sb-directive-start-outside { - border-top-left-radius: 10px; - border-top-right-radius: 10px; - border-style: solid; - border-color: var(--directive-color); - border-left-width: 1px; - border-top-width: 1px; - border-right-width: 1px; - border-bottom-width: 0; - padding-top: 5px !important; + color: transparent !important; + + .sb-directive-placeholder { + color: var(--directive-font-color) !important; + opacity: 0.25; + padding-left: 5ch; + } + + * { + color: transparent !important; + } } .sb-directive-end-outside { - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - border-style: solid; - border-color: var(--directive-color); - border-left-width: 1px; - border-bottom-width: 1px; - border-right-width: 1px; - border-top-width: 0; - padding-bottom: 5px !important; -} + color: transparent !important; -.sb-directive-end-outside.sb-directive-start-outside { - border-top-width: 1px; - border-bottom-width: 1px; + .sb-directive-placeholder { + color: var(--directive-font-color) !important; + opacity: 0.25; + padding-left: 5ch; + } + + * { + color: transparent !important; + } } .sb-emphasis { @@ -473,7 +478,9 @@ sb-admonition-warning .sb-admonition-icon { text-decoration: none; cursor: pointer; } -a.sb-wiki-link-page-missing, .sb-wiki-link-page-missing > .sb-wiki-link-page { + +a.sb-wiki-link-page-missing, +.sb-wiki-link-page-missing>.sb-wiki-link-page { color: #9e4705; background-color: rgba(77, 141, 255, 0.07); border-radius: 5px; @@ -516,8 +523,11 @@ html[data-theme="dark"] { .sb-actions button:hover { color: #37a1ed; } - - .sb-line-h1, .sb-line-h2, .sb-line-h3, .sb-line-h4 { + + .sb-line-h1, + .sb-line-h2, + .sb-line-h3, + .sb-line-h4 { color: #d1d1d1; } @@ -525,7 +535,7 @@ html[data-theme="dark"] { background-color: rgb(41, 40, 35, 0.5); } - .sb-saved > input { + .sb-saved>input { color: rgb(225, 225, 225); } @@ -540,7 +550,7 @@ html[data-theme="dark"] { border-bottom: 1px solid #6c6c6c; } - .sb-line-li .sb-meta ~ .sb-meta, + .sb-line-li .sb-meta~.sb-meta, .sb-line-fenced-code .sb-meta { color: #d17278; } @@ -556,7 +566,7 @@ html[data-theme="dark"] { background-color: #333; } - .sb-notifications > div { + .sb-notifications>div { border: rgb(197, 197, 197) 1px solid; background-color: #333; } @@ -570,11 +580,11 @@ html[data-theme="dark"] { } .sb-table-widget { - + tbody tr:nth-of-type(even) { - background-color: #686868; + background-color: #686868; } } -} +} \ No newline at end of file