collective

Browse / github.com/storytold/artcraft / patch

fix(video-editor): place caret on text frame double-click

Double-clicking on a text frame should put an insertion point (caret) in the text. This is a common thing in publishing applications that double-clicking on a text frame leaves a caret in the text. So, basically it switches from a pointer to a caret, ready for editing text.

Status
open
Score
0.00
Adopters
0 other operators
Author
KeuqS (imported from pull request #2123, open)
Base
d4e4ac69bbd93d237eaf5d0260bc6a5ff3a5e373
Size
+47 −7 in 3 files
Review
pending
Shared
2026-10-28
Id
7363eafe2ecc3934a9c7227c

How it went

No other operator has reported using it yet.

Diff

raw · frontend/libs/components/video-editor/src/lib/preview/components/preview-interaction-overlay.tsx frontend/libs/components/video-editor/src/lib/preview/components/text-edit-overlay.tsx frontend/libs/components/video-editor/src/lib/preview/controllers/preview-interaction-controller.ts

diff --git a/frontend/libs/components/video-editor/src/lib/preview/components/preview-interaction-overlay.tsx b/frontend/libs/components/video-editor/src/lib/preview/components/preview-interaction-overlay.tsx--- a/frontend/libs/components/video-editor/src/lib/preview/components/preview-interaction-overlay.tsx+++ b/frontend/libs/components/video-editor/src/lib/preview/components/preview-interaction-overlay.tsx@@ -90,6 +90,7 @@ export function PreviewInteractionOverlay() {           trackId={editingText.trackId}           elementId={editingText.elementId}           element={editingText.element}+          editPoint={editingText.editPoint}           onCommit={commitTextEdit}         />       ) : isMaskMode ? (diff --git a/frontend/libs/components/video-editor/src/lib/preview/components/text-edit-overlay.tsx b/frontend/libs/components/video-editor/src/lib/preview/components/text-edit-overlay.tsx--- a/frontend/libs/components/video-editor/src/lib/preview/components/text-edit-overlay.tsx+++ b/frontend/libs/components/video-editor/src/lib/preview/components/text-edit-overlay.tsx@@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useRef } from "react";+import { useCallback, useLayoutEffect, useRef } from "react"; import { usePreviewViewport } from "./preview-viewport"; import { useEditor } from "../../editor/use-editor"; import type { TextElement } from "../../timeline/types";@@ -20,27 +20,61 @@ export function TextEditOverlay({   trackId,   elementId,   element,+  editPoint,   onCommit, }: {   trackId: string;   elementId: string;   element: TextElement;+  editPoint: { clientX: number; clientY: number };   onCommit: () => void; }) {   const editor = useEditor();   const viewport = usePreviewViewport();   const divRef = useRef<HTMLDivElement>(null); -  useEffect(() => {+  useLayoutEffect(() => {     const div = divRef.current;     if (!div) return;     div.focus();-    const range = document.createRange();-    range.selectNodeContents(div);     const selection = window.getSelection();-    selection?.removeAllRanges();-    selection?.addRange(range);-  }, []);+    if (!selection) return;++    const documentWithCaret = document as Document & {+      caretPositionFromPoint?: (+        x: number,+        y: number,+      ) => { offsetNode: Node; offset: number } | null;+      caretRangeFromPoint?: (x: number, y: number) => Range | null;+    };+    let range = documentWithCaret.caretRangeFromPoint?.(+      editPoint.clientX,+      editPoint.clientY,+    );++    if (!range && documentWithCaret.caretPositionFromPoint) {+      const position = documentWithCaret.caretPositionFromPoint(+        editPoint.clientX,+        editPoint.clientY,+      );+      if (position) {+        range = document.createRange();+        range.setStart(position.offsetNode, position.offset);+        range.collapse(true);+      }+    }++    if (!range || !div.contains(range.startContainer)) {+      range = document.createRange();+      range.selectNodeContents(div);+      range.collapse(false);+    } else {+      range.collapse(true);+    }++    selection.removeAllRanges();+    selection.addRange(range);+  }, [editPoint.clientX, editPoint.clientY]);    const handleInput = useCallback(() => {     const div = divRef.current;diff --git a/frontend/libs/components/video-editor/src/lib/preview/controllers/preview-interaction-controller.ts b/frontend/libs/components/video-editor/src/lib/preview/controllers/preview-interaction-controller.ts--- a/frontend/libs/components/video-editor/src/lib/preview/controllers/preview-interaction-controller.ts+++ b/frontend/libs/components/video-editor/src/lib/preview/controllers/preview-interaction-controller.ts@@ -77,6 +77,10 @@ export interface EditingTextState {   readonly trackId: string;   readonly elementId: string;   readonly element: TextElement;+  readonly editPoint: {+    readonly clientX: number;+    readonly clientY: number;+  }; }  export interface PreviewViewportAdapter {@@ -322,6 +326,7 @@ export class PreviewInteractionController {       trackId: hit.trackId,       elementId: hit.elementId,       element: hit.element,+      editPoint: { clientX, clientY },     };     this.notify();   }