Repository navigation
feat: seamless inline editing of generated prose #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { describe, it, expect, vi } from 'vitest' | ||
| import { render, screen, fireEvent } from '@testing-library/react' | ||
| import { EditableText } from './EditableText' | ||
|
|
||
| describe('EditableText', () => { | ||
| it('commits trimmed text on blur when changed', () => { | ||
| const onCommit = vi.fn() | ||
| render(<EditableText value="hello" onCommit={onCommit} ariaLabel="Field" />) | ||
| const el = screen.getByRole('textbox', { name: 'Field' }) | ||
| el.textContent = ' world ' | ||
| fireEvent.blur(el) | ||
| expect(onCommit).toHaveBeenCalledWith('world') | ||
| }) | ||
|
|
||
| it('does not commit when the value is unchanged', () => { | ||
| const onCommit = vi.fn() | ||
| render(<EditableText value="hello" onCommit={onCommit} ariaLabel="Field" />) | ||
| const el = screen.getByRole('textbox', { name: 'Field' }) | ||
| fireEvent.blur(el) | ||
| expect(onCommit).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('reverts to the previous value when cleared to empty', () => { | ||
| const onCommit = vi.fn() | ||
| render(<EditableText value="hello" onCommit={onCommit} ariaLabel="Field" />) | ||
| const el = screen.getByRole('textbox', { name: 'Field' }) | ||
| el.textContent = ' ' | ||
| fireEvent.blur(el) | ||
| expect(onCommit).not.toHaveBeenCalled() | ||
| expect(el.textContent).toBe('hello') | ||
| }) | ||
|
|
||
| it('renders read-only text (no textbox) when onCommit is omitted', () => { | ||
| render(<EditableText value="static" ariaLabel="Field" />) | ||
| expect(screen.queryByRole('textbox')).toBeNull() | ||
| expect(screen.getByText('static')).toBeInTheDocument() | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| 'use client' | ||
| import { useRef, type KeyboardEvent, type ClipboardEvent } from 'react' | ||
| import type { EditableTextProps } from './EditableText.types' | ||
|
|
||
| // Seamless inline editing: the text itself is contentEditable — no input box, no | ||
| // edit/save buttons. Edits commit on blur and persist via the run's autosave. | ||
| // React renders `value` as the initial child and never re-renders it mid-edit | ||
| // (commits happen on blur, so the value prop only changes when unfocused), so the | ||
| // caret never jumps. | ||
| export function EditableText({ value, onCommit, multiline = false, ariaLabel, className = '' }: EditableTextProps) { | ||
| const ref = useRef<HTMLDivElement>(null) | ||
|
|
||
| if (!onCommit) { | ||
| return ( | ||
| <div className={className} style={multiline ? { whiteSpace: 'pre-wrap' } : undefined}> | ||
| {value} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| function commit() { | ||
| const el = ref.current | ||
| if (!el) return | ||
| // jsdom lacks innerText; fall back to textContent so tests still exercise commit. | ||
| const raw = (multiline ? el.innerText || el.textContent : el.textContent) || '' | ||
| const next = raw.replace(/ /g, ' ').replace(/\n+$/, '').trim() | ||
| if (!next) { | ||
| el.textContent = value // never persist blank prose — revert | ||
| return | ||
| } | ||
| if (next !== value) onCommit!(next) | ||
| } | ||
|
|
||
| function handleKeyDown(e: KeyboardEvent) { | ||
| if (!multiline && e.key === 'Enter') { | ||
| e.preventDefault() | ||
| ref.current?.blur() | ||
| } | ||
| } | ||
|
|
||
| function handlePaste(e: ClipboardEvent) { | ||
| e.preventDefault() | ||
| const text = e.clipboardData.getData('text/plain') | ||
| document.execCommand('insertText', false, text) | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| ref={ref} | ||
| contentEditable | ||
| suppressContentEditableWarning | ||
| role="textbox" | ||
| aria-label={ariaLabel} | ||
| aria-multiline={multiline || undefined} | ||
| tabIndex={0} | ||
| onBlur={commit} | ||
| onKeyDown={handleKeyDown} | ||
| onPaste={handlePaste} | ||
| className={`-mx-1 cursor-text rounded-sm px-1 outline-none transition-colors hover:bg-muted/40 focus:bg-muted/30 focus:ring-1 focus:ring-primary/40 ${className}`} | ||
| style={multiline ? { whiteSpace: 'pre-wrap' } : undefined} | ||
| > | ||
| {value} | ||
| </div> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export type EditableTextProps = { | ||
| value: string | ||
| /** When omitted, the text renders read-only (no contentEditable). */ | ||
| onCommit?: (next: string) => void | ||
| /** Multiline keeps line breaks and lets Enter insert a newline. */ | ||
| multiline?: boolean | ||
| ariaLabel: string | ||
| className?: string | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { EditableText } from './EditableText' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,14 @@ | ||
| import type { LaunchCore, PlatformContent } from '@/lib/types' | ||
|
|
||
| // Commits an inline edit of one prose field. `path` is relative to the active | ||
| // platform's content object (e.g. ['copy','tagline'], ['thread','tweets',2]). | ||
| export type EditFieldFn = (path: (string | number)[], value: string) => void | ||
|
|
||
| // Every platform's edit-view section group takes the shared core plus its own | ||
| // content (narrowed internally) and a loading flag for the regenerate shimmer. | ||
| export type PlatformSectionsProps = { core: LaunchCore; content: PlatformContent; loading?: boolean } | ||
| // content (narrowed internally), a loading flag, and an optional inline-edit hook. | ||
| export type PlatformSectionsProps = { | ||
| core: LaunchCore | ||
| content: PlatformContent | ||
| loading?: boolean | ||
| onEdit?: EditFieldFn | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Current browser support and deprecation status for Document.execCommand('insertText') in contentEditable elements (Chrome, Firefox, Safari, iOS Safari), and recommended modern replacement API.💡 Result:
As of June 2026, document.execCommand('insertText') is officially deprecated and non-standard [1][2][3]. It is no longer on a standards track, and its use is discouraged by browser vendors and documentation platforms [1][4]. Browser Support Status Despite being deprecated, the method remains widely implemented across major browsers for compatibility reasons [1][5]. - Chrome, Edge, and Opera: Full support [6]. - Firefox: Supported, though with some known historical caveats regarding nested calls and input manipulation in specific older versions [6][7][2]. - Safari (including iOS Safari): Supported [6]. Because it is not standardized, browser vendors have no obligation to maintain it, and it may cease to function at any time [1][4]. Developers should treat it as an unstable feature [1]. Modern Replacement and Recommendations There is no single direct, drop-in replacement API for document.execCommand('insertText') that provides the same behavior, particularly regarding the preservation of the browser's native undo/redo history [1][4][5]. - For general text editing: Developers are encouraged to use the Selection and Range APIs to manually manipulate the DOM or, for complex editors, leverage established open-source rich-text editing libraries (e.g., CKEditor, TinyMCE, ProseMirror, Slate) which abstract away the inconsistencies of native browser editing [4][8][5]. - For advanced custom editing: The EditContext API is the modern, standardized alternative designed to give developers granular control over text input, composition, and selection, without relying on the DOM-heavy contentEditable attribute [9][10][11]. However, EditContext is primarily for building custom editing experiences rather than a direct replacement for simple text insertion [10]. - For undo/redo preservation: If you must maintain native undo/redo behavior, many developers are currently forced to continue using execCommand as a legacy fallback due to the lack of a standardized modern API that satisfies this specific requirement [12][13][5]. Some implementers build custom undo/redo stacks using MutationObserver or application-level state management, though this significantly increases implementation complexity [4][5].
Citations:
document.execCommandhas valid use cases without viable alternatives mdn/content#40245🏁 Script executed:
Repository: AhmedFr/launch-kit
Length of output: 110
🏁 Script executed:
Repository: AhmedFr/launch-kit
Length of output: 1172
🏁 Script executed:
Repository: AhmedFr/launch-kit
Length of output: 115
🏁 Script executed:
# Check the test file to understand the expected behavior cat -n components/common/EditableText/EditableText.test.tsxRepository: AhmedFr/launch-kit
Length of output: 1813
🏁 Script executed:
Repository: AhmedFr/launch-kit
Length of output: 1766
🏁 Script executed:
Repository: AhmedFr/launch-kit
Length of output: 857
Replace deprecated paste insertion API with a fallback-safe approach.
Line 44 uses
document.execCommand('insertText', ...)after preventing default paste at Line 42. This API is officially deprecated and non-standard as of June 2026; if the browser blocks or removes support for it, pasted text will be lost. Switch to the Selection and Range APIs withexecCommandas a fallback to ensure reliable paste handling.Suggested patch
function handlePaste(e: ClipboardEvent) { e.preventDefault() const text = e.clipboardData.getData('text/plain') - document.execCommand('insertText', false, text) + const selection = window.getSelection() + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0) + range.deleteContents() + range.insertNode(document.createTextNode(text)) + range.collapse(false) + selection.removeAllRanges() + selection.addRange(range) + return + } + // Fallback for engines that still support it + document.execCommand('insertText', false, text) }📝 Committable suggestion
🤖 Prompt for AI Agents