Change selection behaviour (#904)

* Change selection behaviour
* turn mouseup/mousedown events into click
* Remove `drawSelection()`
* Add custom cursor
pull/917/head
MrMugame 2024-07-03 19:13:54 +02:00 committed by GitHub
parent 09e7f1f906
commit 5c28b1a73f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 74 additions and 33 deletions

View File

@ -6,7 +6,6 @@ import {
decoratorStateField, decoratorStateField,
invisibleDecoration, invisibleDecoration,
isCursorInRange, isCursorInRange,
shouldRenderAsCode,
} from "./util.ts"; } from "./util.ts";
import { MarkdownWidget } from "./markdown_widget.ts"; import { MarkdownWidget } from "./markdown_widget.ts";
import { IFrameWidget } from "./iframe_widget.ts"; import { IFrameWidget } from "./iframe_widget.ts";
@ -18,7 +17,7 @@ export function fencedCodePlugin(editor: Client) {
syntaxTree(state).iterate({ syntaxTree(state).iterate({
enter({ from, to, name, node }) { enter({ from, to, name, node }) {
if (name === "FencedCode") { if (name === "FencedCode") {
if (shouldRenderAsCode(state, [from, to])) { if (isCursorInRange(state, [from, to])) {
// Don't render the widget if the cursor is inside the fenced code // Don't render the widget if the cursor is inside the fenced code
return; return;
} }

View File

@ -24,10 +24,6 @@ export class LinkWidget extends WidgetType {
// Mouse handling // Mouse handling
anchor.addEventListener("click", (e) => { anchor.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
});
anchor.addEventListener("mouseup", (e) => {
if (e.button !== 0) { if (e.button !== 0) {
return; return;
} }
@ -57,6 +53,13 @@ export class LinkWidget extends WidgetType {
anchor.href = this.options.href || "#"; anchor.href = this.options.href || "#";
return anchor; return anchor;
} }
eq(other: WidgetType): boolean {
return other instanceof LinkWidget &&
this.options.text === other.options.text &&
this.options.href === other.options.href &&
this.options.title === other.options.title;
}
} }
export class HtmlWidget extends WidgetType { export class HtmlWidget extends WidgetType {
@ -89,10 +92,8 @@ export function decoratorStateField(
}, },
update(value: DecorationSet, tr: Transaction) { update(value: DecorationSet, tr: Transaction) {
// if (tr.docChanged || tr.selection) { if (tr.isUserEvent("select.pointer")) return value;
return stateToDecoratorMapper(tr.state); return stateToDecoratorMapper(tr.state);
// }
// return value;
}, },
provide: (f) => EditorView.decorations.from(f), provide: (f) => EditorView.decorations.from(f),
@ -161,21 +162,6 @@ export function isCursorInRange(state: EditorState, range: [number, number]) {
); );
} }
export function shouldRenderAsCode(
state: EditorState,
range: [number, number],
) {
const mainSelection = state.selection.main;
// When the selection is empty, we need to check if the cursor is inside the fenced code
if (mainSelection.empty) {
return checkRangeOverlap(range, [mainSelection.from, mainSelection.to]);
} else {
// If the selection is encompassing the fenced code we render as code, or vice versa
return checkRangeSubset([mainSelection.from, mainSelection.to], range) ||
checkRangeSubset(range, [mainSelection.from, mainSelection.to]);
}
}
/** /**
* Decoration to simply hide anything. * Decoration to simply hide anything.
*/ */

View File

@ -86,9 +86,11 @@ export function cleanWikiLinkPlugin(client: Client) {
callback: (e) => { callback: (e) => {
if (e.altKey) { if (e.altKey) {
// Move cursor into the link // Move cursor into the link
return client.editorView.dispatch({ client.editorView.dispatch({
selection: { anchor: from + firstMark.length }, selection: { anchor: from + firstMark.length },
}); });
client.focus();
return;
} }
// Dispatch click event to navigate there without moving the cursor // Dispatch click event to navigate there without moving the cursor
const clickEvent: ClickEvent = { const clickEvent: ClickEvent = {

View File

@ -19,14 +19,15 @@ import {
LanguageSupport, LanguageSupport,
syntaxHighlighting, syntaxHighlighting,
} from "@codemirror/language"; } from "@codemirror/language";
import { EditorState } from "@codemirror/state"; import { EditorSelection, EditorState } from "@codemirror/state";
import { import {
drawSelection,
dropCursor, dropCursor,
EditorView, EditorView,
highlightSpecialChars, highlightSpecialChars,
KeyBinding, KeyBinding,
keymap, keymap,
layer,
RectangleMarker,
ViewPlugin, ViewPlugin,
ViewUpdate, ViewUpdate,
} from "@codemirror/view"; } from "@codemirror/view";
@ -115,7 +116,6 @@ export function createEditorState(
codeCopyPlugin(client), codeCopyPlugin(client),
highlightSpecialChars(), highlightSpecialChars(),
history(), history(),
drawSelection(),
dropCursor(), dropCursor(),
codeFolding({ codeFolding({
placeholderText: "…", placeholderText: "…",
@ -124,8 +124,46 @@ export function createEditorState(
...cleanModePlugins(client), ...cleanModePlugins(client),
EditorView.lineWrapping, EditorView.lineWrapping,
plugLinter(client), plugLinter(client),
// lintGutter(), // Taken from https://github.com/codemirror/view/blob/main/src/draw-selection.ts
// gutters(), layer({
above: true,
markers(view) {
const safari = /Apple Computer/.test(navigator.vendor);
const ios = safari &&
(/Mobile\/\w+/.test(navigator.userAgent) ||
navigator.maxTouchPoints > 2);
const { state } = view;
const cursors = [];
for (const r of state.selection.ranges) {
const prim = r == state.selection.main;
if (!r.empty || !prim || !ios) {
const className = prim
? "cm-cursor cm-cursor-primary"
: "cm-cursor cm-cursor-secondary";
const cursor = r.empty
? r
: EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
for (
const piece of RectangleMarker.forRange(view, className, cursor)
) cursors.push(piece);
}
}
return cursors;
},
update(update, dom) {
if (update.transactions.some((tr) => tr.selection)) {
dom.style.animationName = dom.style.animationName == "cm-blink"
? "cm-blink2"
: "cm-blink";
}
return update.docChanged || update.selectionSet;
},
mount(dom) {
dom.style.animationDuration = "1200ms";
},
class: "cm-cursorLayer",
}),
postScriptPrefacePlugin(client), postScriptPrefacePlugin(client),
lineWrapper([ lineWrapper([
{ selector: "ATXHeading1", class: "sb-line-h1" }, { selector: "ATXHeading1", class: "sb-line-h1" },
@ -200,7 +238,7 @@ export function createEditorState(
touchCount = 0; touchCount = 0;
}, },
mousedown: (event: MouseEvent, view: EditorView) => { click: (event: MouseEvent, view: EditorView) => {
const pos = view.posAtCoords(event); const pos = view.posAtCoords(event);
if (event.button !== 0) { if (event.button !== 0) {
return; return;

View File

@ -60,6 +60,12 @@ export class MainUI {
}); });
} }
}); });
globalThis.addEventListener("mouseup", (_) => {
setTimeout(() => {
client.editorView.dispatch({});
})
});
} }
ViewComponent() { ViewComponent() {

View File

@ -4,7 +4,8 @@
color: var(--root-color); color: var(--root-color);
} }
.cm-cursor { .cm-cursor,
.cm-dropCursor {
border-left: 1.2px solid var(--editor-caret-color) !important; border-left: 1.2px solid var(--editor-caret-color) !important;
} }
@ -174,9 +175,10 @@
#sb-editor { #sb-editor {
.cm-content { .cm-content {
font-family: var(--editor-font); font-family: var(--editor-font);
caret-color: transparent;
} }
.cm-selectionBackground { ::selection {
background-color: var(--editor-selection-background-color); background-color: var(--editor-selection-background-color);
} }

View File

@ -401,6 +401,10 @@
} }
.sb-line-code-outside .sb-code-info { .sb-line-code-outside .sb-code-info {
&::selection {
background-color: transparent;
}
display: block; display: block;
float: right; float: right;
font-size: 90%; font-size: 90%;
@ -408,6 +412,10 @@
} }
.sb-code-copy-button { .sb-code-copy-button {
&::selection {
background-color: transparent;
}
float: right; float: right;
cursor: pointer; cursor: pointer;
margin: 0 3px; margin: 0 3px;