дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(ui): Make read-only content selectable with a Copy context menu (#48)
Root cause: body is user-select: none and each feature had to opt back in; the Markdown preview (and other read-only views) never did, and the app-wide contextmenu guard left no Copy item. Fix: shared selectableText class (src/ui) for read-only content, and a fallback Copy context menu from browserGuards when right-clicking a selection or a selectable area (component menus still win). Siblings covered: Quick Documentation / hover / completion docs, hover errors, Quick Definition, debugger value tooltip, inspection messages, message dialogs (no drag on body text), notification titles, About dialog; existing selectable consoles now get Copy too. Verified in-app on macOS: select in Markdown preview, right-click Copy puts the text on the clipboard; tab context menu unchanged. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
6dfd82de4c
коміт
de84a643aa
17 files changed
+453
-55
No files matched your search
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { hideContextMenu, selectableText, showContextMenu, useContextMenuStore } from "@/ui";
|
||||
import { installBrowserGuards } from "./browserGuards";
|
||||
|
||||
let dispose: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
dispose = installBrowserGuards({ blockReload: false });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dispose();
|
||||
hideContextMenu();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function rightClick(el: Element) {
|
||||
const e = new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: 5, clientY: 6 });
|
||||
el.dispatchEvent(e);
|
||||
return e;
|
||||
}
|
||||
|
||||
describe("browser guards: context menu", () => {
|
||||
it("never shows the native menu, and nothing on chrome", () => {
|
||||
document.body.innerHTML = `<button id="b">Run</button>`;
|
||||
const e = rightClick(document.getElementById("b")!);
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
expect(useContextMenuStore.getState().menu).toBeNull();
|
||||
});
|
||||
|
||||
it("shows Copy on selectable read-only text (issue #36)", () => {
|
||||
document.body.innerHTML = `<div class="${selectableText}"><p id="p">Hello</p></div>`;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(document.getElementById("p")!);
|
||||
window.getSelection()!.addRange(range);
|
||||
const e = rightClick(document.getElementById("p")!);
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
const menu = useContextMenuStore.getState().menu;
|
||||
expect(menu).toMatchObject({ x: 5, y: 6 });
|
||||
expect(menu!.items[0]).toMatchObject({ label: "Copy", disabled: false });
|
||||
});
|
||||
|
||||
it("keeps a component's own context menu", () => {
|
||||
document.body.innerHTML = `<div class="${selectableText}"><p id="p">Hello</p></div>`;
|
||||
const p = document.getElementById("p")!;
|
||||
p.addEventListener("contextmenu", (e) => showContextMenu(e, [{ label: "Own" }]));
|
||||
rightClick(p);
|
||||
expect(useContextMenuStore.getState().menu!.items[0]).toMatchObject({ label: "Own" });
|
||||
});
|
||||
|
||||
it("keeps out when a handler already prevented the default", () => {
|
||||
document.body.innerHTML = `<div class="${selectableText}"><p id="p">Hello</p></div>`;
|
||||
const p = document.getElementById("p")!;
|
||||
p.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
rightClick(p);
|
||||
expect(useContextMenuStore.getState().menu).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -2,8 +2,10 @@
|
||||
* Makes the webview behave like a desktop app: never the browser context menu
|
||||
* (its Reload/AutoFill items would act on the webview), no reload/print/
|
||||
* save-page/history shortcuts, no navigation on file drop. Copy/paste/undo
|
||||
* shortcuts stay untouched.
|
||||
* shortcuts stay untouched. A right-click no component handled on selected or
|
||||
* selectable read-only text gets IDEA's Copy menu instead of nothing.
|
||||
*/
|
||||
import { copyMenuFor, showContextMenu, useContextMenuStore } from "@/ui";
|
||||
|
||||
const BLOCKED = new Set([
|
||||
"Meta+P",
|
||||
@@ -35,7 +37,17 @@ function keyId(e: KeyboardEvent) {
|
||||
|
||||
export function installBrowserGuards(opts: { blockReload?: boolean } = {}): () => void {
|
||||
const blockReload = opts.blockReload ?? import.meta.env.PROD;
|
||||
const onContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||
let menuBefore: unknown = null;
|
||||
const beforeContextMenu = () => {
|
||||
menuBefore = useContextMenuStore.getState().menu;
|
||||
};
|
||||
const onContextMenu = (e: MouseEvent) => {
|
||||
const handled = e.defaultPrevented || useContextMenuStore.getState().menu !== menuBefore;
|
||||
e.preventDefault();
|
||||
if (handled) return;
|
||||
const items = copyMenuFor(e.target);
|
||||
if (items) showContextMenu(e, items);
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const id = keyId(e);
|
||||
if (BLOCKED.has(id) || (blockReload && RELOAD.has(id))) e.preventDefault();
|
||||
@@ -43,11 +55,13 @@ export function installBrowserGuards(opts: { blockReload?: boolean } = {}): () =
|
||||
const onDrag = (e: DragEvent) => {
|
||||
if (!(e.target as Element | null)?.closest?.("[data-drop-target]")) e.preventDefault();
|
||||
};
|
||||
window.addEventListener("contextmenu", beforeContextMenu, true);
|
||||
window.addEventListener("contextmenu", onContextMenu);
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("dragover", onDrag);
|
||||
window.addEventListener("drop", onDrag);
|
||||
return () => {
|
||||
window.removeEventListener("contextmenu", beforeContextMenu, true);
|
||||
window.removeEventListener("contextmenu", onContextMenu);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("dragover", onDrag);
|
||||
|
||||
Reference in new issue
Заблокувати користувача