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
@@ -90,6 +90,7 @@ export function PreviewInteractionOverlay() { trackId={editingText.trackId} elementId={editingText.elementId} element={editingText.element}+ editPoint={editingText.editPoint} onCommit={commitTextEdit} /> ) : isMaskMode ? (@@ -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;@@ -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(); }