From d79883333c5593093ab6b27b1f7f5753a9381855 Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Thu, 28 Nov 2024 16:29:25 +0530 Subject: [PATCH] fix: hide cursor when editor not focused --- .../extensions/smooth-cursor/plugin-better.ts | 103 ------------------ .../extensions/smooth-cursor/plugin-good.ts | 97 ----------------- .../core/extensions/smooth-cursor/plugin.ts | 30 ++++- packages/editor/src/styles/editor.css | 12 -- 4 files changed, 25 insertions(+), 217 deletions(-) delete mode 100644 packages/editor/src/core/extensions/smooth-cursor/plugin-better.ts delete mode 100644 packages/editor/src/core/extensions/smooth-cursor/plugin-good.ts diff --git a/packages/editor/src/core/extensions/smooth-cursor/plugin-better.ts b/packages/editor/src/core/extensions/smooth-cursor/plugin-better.ts deleted file mode 100644 index 6fdd4414746..00000000000 --- a/packages/editor/src/core/extensions/smooth-cursor/plugin-better.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { type Selection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"; -import { type EditorView, Decoration, DecorationSet } from "@tiptap/pm/view"; - -export const PROSEMIRROR_SMOOTH_CURSOR_CLASS = "prosemirror-smooth-cursor"; - -export function smoothCursorPlugin(): Plugin { - let smoothCursor: HTMLElement | null = typeof document === "undefined" ? null : document.createElement("div"); - - return new Plugin({ - key, - view: (view) => { - const doc = view.dom.ownerDocument; - smoothCursor = smoothCursor || document.createElement("div"); - const cursor = smoothCursor; - - const update = () => { - updateCursor(view, cursor); - }; - - let observer: ResizeObserver | undefined; - if (window.ResizeObserver) { - observer = new window.ResizeObserver(update); - observer?.observe(view.dom); - } - - doc.addEventListener("selectionchange", update); - - return { - update, - destroy: () => { - doc.removeEventListener("selectionchange", update); - observer?.unobserve(view.dom); - }, - }; - }, - props: { - decorations: (state) => { - if (!smoothCursor || !isTextSelection(state.selection) || !state.selection.empty) return; - - return DecorationSet.create(state.doc, [ - Decoration.widget(0, smoothCursor, { - key: PROSEMIRROR_SMOOTH_CURSOR_CLASS, - }), - ]); - }, - - attributes: { - class: "smooth-cursor-enabled", - }, - }, - }); -} - -const key = new PluginKey(PROSEMIRROR_SMOOTH_CURSOR_CLASS); - -function getCursorRect( - view: EditorView, - toStart: boolean -): { left: number; right: number; top: number; bottom: number } | null { - const selection = window.getSelection(); - if (!selection || !selection.rangeCount) return null; - - const range = selection?.getRangeAt(0)?.cloneRange(); - if (!range) return null; - - range.collapse(toStart); - const rects = range.getClientRects(); - const rect = rects?.length ? rects[rects.length - 1] : null; - if (rect?.height) return rect; - - return view.coordsAtPos(view.state.selection.head); -} - -function isTextSelection(selection: Selection): selection is TextSelection { - return selection && typeof selection === "object" && "$cursor" in selection; -} - -function updateCursor(view?: EditorView, cursor?: HTMLElement) { - if (!view || !view.dom || view.isDestroyed || !cursor) return; - - const { state, dom } = view; - const { selection } = state; - if (!isTextSelection(selection)) return; - - const cursorRect = getCursorRect(view, selection.$head === selection.$from); - - if (!cursorRect) return cursor; - - const editorRect = dom.getBoundingClientRect(); - - const className = PROSEMIRROR_SMOOTH_CURSOR_CLASS; - - cursor.className = className; - cursor.style.height = `${cursorRect.bottom - cursorRect.top}px`; - - // Calculate the exact position - const x = cursorRect.left - editorRect.left; - const y = cursorRect.top - editorRect.top; - - requestAnimationFrame(() => { - cursor.style.transform = `translate3d(${x}px, ${y}px, 0)`; - }); -} diff --git a/packages/editor/src/core/extensions/smooth-cursor/plugin-good.ts b/packages/editor/src/core/extensions/smooth-cursor/plugin-good.ts deleted file mode 100644 index 12089cc3de7..00000000000 --- a/packages/editor/src/core/extensions/smooth-cursor/plugin-good.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { type Selection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"; -import { type EditorView, Decoration, DecorationSet } from "@tiptap/pm/view"; - -export const PROSEMIRROR_SMOOTH_CURSOR_CLASS = "prosemirror-smooth-cursor"; - -export function smoothCursorPlugin(): Plugin { - let smoothCursor: HTMLElement | null = typeof document === "undefined" ? null : document.createElement("div"); - - return new Plugin({ - key, - view: (view) => { - const doc = view.dom.ownerDocument; - smoothCursor = smoothCursor || document.createElement("div"); - const cursor = smoothCursor; - - const update = () => { - updateCursor(view, cursor); - }; - - let observer: ResizeObserver | undefined; - if (window.ResizeObserver) { - observer = new window.ResizeObserver(update); - observer?.observe(view.dom); - } - - doc.addEventListener("selectionchange", update); - - return { - update, - destroy: () => { - doc.removeEventListener("selectionchange", update); - observer?.unobserve(view.dom); - }, - }; - }, - props: { - decorations: (state) => { - if (!smoothCursor || !isTextSelection(state.selection) || !state.selection.empty) return; - - return DecorationSet.create(state.doc, [ - Decoration.widget(0, smoothCursor, { - key: PROSEMIRROR_SMOOTH_CURSOR_CLASS, - }), - ]); - }, - - attributes: { - class: "smooth-cursor-enabled", - }, - }, - }); -} - -const key = new PluginKey(PROSEMIRROR_SMOOTH_CURSOR_CLASS); - -function getCursorRect( - view: EditorView, - toStart: boolean -): { left: number; right: number; top: number; bottom: number } | null { - const selection = window.getSelection(); - if (!selection || !selection.rangeCount) return null; - - const range = selection?.getRangeAt(0)?.cloneRange(); - if (!range) return null; - - range.collapse(toStart); - const rects = range.getClientRects(); - const rect = rects?.length ? rects[rects.length - 1] : null; - if (rect?.height) return rect; - - return view.coordsAtPos(view.state.selection.head); -} - -function isTextSelection(selection: Selection): selection is TextSelection { - return selection && typeof selection === "object" && "$cursor" in selection; -} - -function updateCursor(view?: EditorView, cursor?: HTMLElement) { - if (!view || !view.dom || view.isDestroyed || !cursor) return; - - const { state, dom } = view; - const { selection } = state; - if (!isTextSelection(selection)) return; - - const cursorRect = getCursorRect(view, selection.$head === selection.$from); - - if (!cursorRect) return cursor; - - const editorRect = dom.getBoundingClientRect(); - - const className = PROSEMIRROR_SMOOTH_CURSOR_CLASS; - - cursor.className = className; - cursor.style.height = `${cursorRect.bottom - cursorRect.top}px`; - cursor.style.left = `${cursorRect.left - editorRect.left}px`; - cursor.style.top = `${cursorRect.top - editorRect.top}px`; -} diff --git a/packages/editor/src/core/extensions/smooth-cursor/plugin.ts b/packages/editor/src/core/extensions/smooth-cursor/plugin.ts index 581cf5dba0b..9d62a0db5ca 100644 --- a/packages/editor/src/core/extensions/smooth-cursor/plugin.ts +++ b/packages/editor/src/core/extensions/smooth-cursor/plugin.ts @@ -6,10 +6,18 @@ export const PROSEMIRROR_SMOOTH_CURSOR_CLASS = "prosemirror-smooth-cursor"; export function smoothCursorPlugin(): Plugin { let smoothCursor: HTMLElement | null = typeof document === "undefined" ? null : document.createElement("div"); let rafId: number | undefined; + let isEditorFocused = false; function updateCursor(view?: EditorView, cursor?: HTMLElement) { if (!view || !view.dom || view.isDestroyed || !cursor) return; + // Hide cursor if editor is not focused + if (!isEditorFocused) { + cursor.style.display = "none"; + return; + } + cursor.style.display = "block"; + const { state, dom } = view; const { selection } = state; if (!isTextSelection(selection)) return; @@ -43,12 +51,21 @@ export function smoothCursorPlugin(): Plugin { const update = () => { if (rafId !== undefined) { - console.log("cleaning up 1: " + rafId); cancelAnimationFrame(rafId); } updateCursor(view, cursor); }; + const handleFocus = () => { + isEditorFocused = true; + update(); + }; + + const handleBlur = () => { + isEditorFocused = false; + update(); + }; + let observer: ResizeObserver | undefined; if (window.ResizeObserver) { observer = new window.ResizeObserver(update); @@ -56,15 +73,18 @@ export function smoothCursorPlugin(): Plugin { } doc.addEventListener("selectionchange", update); + view.dom.addEventListener("focus", handleFocus); + view.dom.addEventListener("blur", handleBlur); return { update, destroy: () => { doc.removeEventListener("selectionchange", update); + view.dom.removeEventListener("focus", handleFocus); + view.dom.removeEventListener("blur", handleBlur); observer?.unobserve(view.dom); // Clean up any pending animation frame if (rafId !== undefined) { - console.log("cleaning up 2: " + rafId); cancelAnimationFrame(rafId); } }, @@ -81,9 +101,9 @@ export function smoothCursorPlugin(): Plugin { ]); }, - attributes: { - class: "smooth-cursor-enabled", - }, + attributes: () => ({ + class: isEditorFocused ? "smooth-cursor-enabled" : "", + }), }, }); } diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index eea32f8b670..c7f196356ff 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -478,17 +478,6 @@ p + p { [data-background-color="purple"] { background-color: var(--editor-colors-purple-background); } -/* end background colors */ -@keyframes caret-flash-smooth { - 0%, - 100% { - opacity: 0; - } - - 50% { - opacity: 1; - } -} .smooth-cursor-enabled { caret-color: transparent; @@ -504,5 +493,4 @@ p + p { transition: transform 0.2s var(--ease-out-quart); will-change: transform; opacity: 0.8; - animation: cursorPulse 0.8s ease-in-out infinite; }