fix(editor): let CodeMirror own caret layout
Keep caret and placeholder behavior on CodeMirror's standard extensions, and move height and scrolling to the editor elements so empty space stays interactive across browsers.\n\n- disable unused multi-cursor selection\n- upgrade CodeMirror state and view patches
This commit is contained in:
+2
-2
@@ -21,8 +21,8 @@
|
||||
"@codemirror/commands": "^6.10.4",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/view": "^6.43.3",
|
||||
"@codemirror/state": "^6.7.1",
|
||||
"@codemirror/view": "^6.43.6",
|
||||
"@connectrpc/connect": "^2.1.1",
|
||||
"@connectrpc/connect-web": "^2.1.1",
|
||||
"@github/relative-time-element": "^5.0.0",
|
||||
|
||||
Generated
+36
-31
@@ -21,11 +21,11 @@ importers:
|
||||
specifier: ^6.12.4
|
||||
version: 6.12.4
|
||||
'@codemirror/state':
|
||||
specifier: ^6.7.0
|
||||
version: 6.7.0
|
||||
specifier: ^6.7.1
|
||||
version: 6.7.1
|
||||
'@codemirror/view':
|
||||
specifier: ^6.43.3
|
||||
version: 6.43.3
|
||||
specifier: ^6.43.6
|
||||
version: 6.43.6
|
||||
'@connectrpc/connect':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1(@bufbuild/protobuf@2.12.0)
|
||||
@@ -498,11 +498,11 @@ packages:
|
||||
'@codemirror/lint@6.9.7':
|
||||
resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==}
|
||||
|
||||
'@codemirror/state@6.7.0':
|
||||
resolution: {integrity: sha512-Zbl9NyscLMZkfXPQnNAIIAFftidrA1UbcJEIMp24C0Bukc2I5T8wJS0wsXYsnDOqCFJUeJ1BITGNs5CqPDSmSg==}
|
||||
'@codemirror/state@6.7.1':
|
||||
resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==}
|
||||
|
||||
'@codemirror/view@6.43.3':
|
||||
resolution: {integrity: sha512-MwEwCAr/o0agJefhC2+reBv5kfOQpMcDRUNQrRYZgWlhH8IwQcerMZrpqWyUFSyO0ebgN2cnh/w87F7G4BGSng==}
|
||||
'@codemirror/view@6.43.6':
|
||||
resolution: {integrity: sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==}
|
||||
|
||||
'@connectrpc/connect-web@2.1.1':
|
||||
resolution: {integrity: sha512-J8317Q2MaFRCT1jzVR1o06bZhDIBmU0UAzWx6xOIXzOq8+k71/+k7MUF7AwcBUX+34WIvbm5syRgC5HXQA8fOg==}
|
||||
@@ -633,8 +633,8 @@ packages:
|
||||
'@lezer/markdown@1.6.4':
|
||||
resolution: {integrity: sha512-N0SxazMj4k65DBfaf1azqtMZd6u7MqluP84/NZnB/io8Td9aleFmAhz9hcbvSfsxT5tdYlJ5qgv5aMJGY4zEtA==}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
'@marijn/find-cluster-break@1.0.3':
|
||||
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
||||
|
||||
'@mermaid-js/parser@1.1.0':
|
||||
resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==}
|
||||
@@ -1828,6 +1828,9 @@ packages:
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
crelt@1.0.7:
|
||||
resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==}
|
||||
|
||||
css-tree@3.2.1:
|
||||
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
@@ -3360,22 +3363,22 @@ snapshots:
|
||||
'@codemirror/autocomplete@6.20.3':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/view': 6.43.3
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.6
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@codemirror/commands@6.10.4':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/view': 6.43.3
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.6
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@codemirror/lang-css@6.3.1':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/state': 6.7.1
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/css': 1.3.3
|
||||
|
||||
@@ -3385,8 +3388,8 @@ snapshots:
|
||||
'@codemirror/lang-css': 6.3.1
|
||||
'@codemirror/lang-javascript': 6.2.5
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/view': 6.43.3
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.6
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/css': 1.3.3
|
||||
'@lezer/html': 1.3.13
|
||||
@@ -3396,8 +3399,8 @@ snapshots:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/lint': 6.9.7
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/view': 6.43.3
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.6
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/javascript': 1.5.4
|
||||
|
||||
@@ -3406,15 +3409,15 @@ snapshots:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/lang-html': 6.4.11
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/view': 6.43.3
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.6
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/markdown': 1.6.4
|
||||
|
||||
'@codemirror/language@6.12.4':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/view': 6.43.3
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.6
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.10
|
||||
@@ -3422,18 +3425,18 @@ snapshots:
|
||||
|
||||
'@codemirror/lint@6.9.7':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.0
|
||||
'@codemirror/view': 6.43.3
|
||||
'@codemirror/state': 6.7.1
|
||||
'@codemirror/view': 6.43.6
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/state@6.7.0':
|
||||
'@codemirror/state@6.7.1':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
'@marijn/find-cluster-break': 1.0.3
|
||||
|
||||
'@codemirror/view@6.43.3':
|
||||
'@codemirror/view@6.43.6':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.7.0
|
||||
crelt: 1.0.6
|
||||
'@codemirror/state': 6.7.1
|
||||
crelt: 1.0.7
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
@@ -3572,7 +3575,7 @@ snapshots:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.3
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
'@marijn/find-cluster-break@1.0.3': {}
|
||||
|
||||
'@mermaid-js/parser@1.1.0':
|
||||
dependencies:
|
||||
@@ -4657,6 +4660,8 @@ snapshots:
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
crelt@1.0.7: {}
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
|
||||
@@ -12,14 +12,11 @@
|
||||
& .cm-editor {
|
||||
background: transparent;
|
||||
font-size: 1rem;
|
||||
/* Fill the host so the whole visible editor area belongs to CodeMirror.
|
||||
Without this the editor only grows to its content height, and any empty
|
||||
space below the text (notably in focus mode, where the host is tall) is
|
||||
bare host markup — clicking it can't focus the editor or place the caret,
|
||||
so the caret appears to vanish in "empty" space (#6076). height:100%
|
||||
collapses to auto when the host is auto-height (normal mode), so this
|
||||
only fills when the host has a definite height. */
|
||||
max-height: 50vh;
|
||||
}
|
||||
&[data-focus-mode] .cm-editor {
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
& .cm-editor.cm-focused {
|
||||
outline: none;
|
||||
@@ -27,7 +24,6 @@
|
||||
& .cm-content {
|
||||
font-family: inherit;
|
||||
padding: 0;
|
||||
caret-color: var(--foreground);
|
||||
}
|
||||
& .cm-line {
|
||||
padding: 0;
|
||||
@@ -35,6 +31,7 @@
|
||||
& .cm-scroller {
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
overflow: auto;
|
||||
}
|
||||
& .cm-placeholder {
|
||||
color: var(--muted-foreground);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { indentUnit } from "@codemirror/language";
|
||||
import { EditorState, type Extension } from "@codemirror/state";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { placeholder as cmPlaceholder, drawSelection, dropCursor, EditorView, type KeyBinding, keymap } from "@codemirror/view";
|
||||
import { GFM } from "@lezer/markdown";
|
||||
import { headingDecorations } from "./headingDecorations";
|
||||
@@ -50,15 +50,11 @@ export function buildEditorExtensions({ placeholder, onChange, onUpdate, onSubmi
|
||||
];
|
||||
|
||||
return [
|
||||
// Core editing behavior. Without these the editor relies on raw
|
||||
// contenteditable: typing works but there is no visible caret on focus
|
||||
// (drawSelection), no undo/redo (history), and Enter/selection/word-motion
|
||||
// keys are unwired (defaultKeymap). They are NOT part of CodeMirror's
|
||||
// minimal core — basicSetup bundles them, and we assemble them here.
|
||||
// Core editing behavior. These are the pieces from CM6 setup that this memo
|
||||
// editor uses, without enabling multi-cursor selection.
|
||||
history(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
// Indent with spaces (markdown), matching the 2-space bullet nesting.
|
||||
indentUnit.of(" "),
|
||||
markdown({ extensions: [GFM] }),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { EditorView } from "@codemirror/view";
|
||||
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef } from "react";
|
||||
import { useTagCounts } from "@/hooks/useUserQueries";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { EDITOR_HEIGHT } from "../constants";
|
||||
import type { EditorController } from "../types/editorController";
|
||||
import { createController } from "./controller";
|
||||
import "./editor.css";
|
||||
@@ -78,13 +77,10 @@ const Editor = forwardRef(function Editor(props: EditorProps, ref: React.Forward
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col justify-start items-start relative w-full bg-inherit",
|
||||
isFocusMode ? "flex-1" : `h-auto ${EDITOR_HEIGHT.normal}`,
|
||||
className,
|
||||
)}
|
||||
className={cn("relative flex w-full flex-col items-start justify-start bg-inherit", isFocusMode && "min-h-0 flex-1", className)}
|
||||
data-focus-mode={isFocusMode || undefined}
|
||||
>
|
||||
<div ref={hostRef} className={cn("w-full text-base overflow-y-auto", isFocusMode ? "flex-1 h-0" : "h-full")} onPaste={onPaste} />
|
||||
<div ref={hostRef} className={cn("w-full text-base", isFocusMode && "min-h-0 flex-1")} onPaste={onPaste} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,11 +8,6 @@ export const FOCUS_MODE_STYLES = {
|
||||
exitButton: "absolute top-2 right-2 z-10 opacity-60 hover:opacity-100",
|
||||
} as const;
|
||||
|
||||
export const EDITOR_HEIGHT = {
|
||||
// Max height for normal mode - focus mode uses flex-1 to grow dynamically
|
||||
normal: "max-h-[50vh]",
|
||||
} as const;
|
||||
|
||||
// localStorage key for the user's preference to show the formatting toolbar in
|
||||
// normal (non-focus) mode. Defaults to off.
|
||||
export const FORMATTING_TOOLBAR_STORAGE_KEY = "memos-editor-formatting-toolbar";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildEditorExtensions } from "@/components/MemoEditor/Editor/extensions";
|
||||
|
||||
describe("MemoEditor CodeMirror extensions", () => {
|
||||
const views: EditorView[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const view of views.splice(0)) {
|
||||
view.destroy();
|
||||
}
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
it("uses CodeMirror's selection and placeholder extensions without enabling multi-cursor selection", () => {
|
||||
const parent = document.body.appendChild(document.createElement("div"));
|
||||
const state = EditorState.create({
|
||||
doc: "",
|
||||
extensions: buildEditorExtensions({
|
||||
placeholder: "Any thoughts...",
|
||||
onChange: vi.fn(),
|
||||
onUpdate: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
getTags: () => [],
|
||||
}),
|
||||
});
|
||||
const view = new EditorView({ state, parent });
|
||||
views.push(view);
|
||||
|
||||
expect(view.state.facet(EditorState.allowMultipleSelections)).toBe(false);
|
||||
expect(view.dom.querySelector(".cm-selectionLayer")).not.toBeNull();
|
||||
expect(view.dom.querySelector(".cm-cursorLayer")).not.toBeNull();
|
||||
expect(view.contentDOM).toHaveAttribute("aria-placeholder", "Any thoughts...");
|
||||
expect(view.dom.querySelector(".cm-placeholder")).toHaveTextContent("Any thoughts...");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user