Add an inline-only mode to the rich text editor - #6214
Conversation
39ba7fd to
01db6df
Compare
AlexVelezLl
left a comment
There was a problem hiding this comment.
Thanks, this is working great. I was wrong when I raised the issue suggesting line-break support; let's drop it. inlineOnly will also mean just a one-line editor.
| // is marked afresh as a slice with no context. A paste with no blocks, like the | ||
| // toolbar's copy of a phrase, is marked too, so its edge spaces keep it apart from | ||
| // the words it lands beside. | ||
| export function unwrapPastedBlocks(doc) { |
There was a problem hiding this comment.
suggestion: Keeping a paste's edge spaces depends on data-pm-slice, which is prosemirror-view's internal clipboard marker, not a documented API. It's written in three places (here, handleCopy in useToolbarActions.js:180, and the clipboardSerializer override in useEditor.js), and its "0 0 []" format is parsed by private code in prosemirror-view's clipboard.ts. Together with trimSourceEdges / collapseWhitespace / collapseCodeWhitespace, that's most of this file's complexity, for behavior the issue doesn't ask for (it asks for blocks flattened to one inline run; edge-space fidelity on copy/paste isn't in the criteria).
The tests run against the real prosemirror-view, so a format change would fail CI rather than break silently. Still, would plain flattening (unwrap blocks, let ProseMirror collapse whitespace) be enough for gap choices, which are short phrases typed into a widget? If the edge spaces matter, a short comment linking the prosemirror-view source this relies on would help whoever upgrades it.
There was a problem hiding this comment.
Dropped edge-space preservation in 9ed37b5: plain flattening now, ProseMirror collapses the whitespace.
- Searched the branch for
data-pm-slice: 3 writers (this file,handleCopy, theclipboardSerializeroverride); all removed. trimSourceEdges,collapseWhitespace,collapseCodeWhitespaceand the edge-space tests are gone.- The one remaining use strips the attribute from an incoming paste, so ProseMirror collapses it like any other paste; it links the prosemirror-view source.
| // Inline-only fields (`qti-gap-text`) leave every block node out of the schema, | ||
| // so no command, shortcut or input rule has a block to create. |
There was a problem hiding this comment.
Please do not mention qti-gap-text here; it should know nothing about the QTI editor. This should be general.
There was a problem hiding this comment.
Removed in 9ed37b5. Searched TipTapEditor/ and docs/rich_text_editor.md for qti-gap-text/GapTextDType: 4 places (this comment, InlineDocument.js, the inlineOnly prop doc, docs/rich_text_editor.md) plus the useEditor.spec.js describe text and GAP_TEXT_ELEMENTS; all reworded as general inline-only content.
| // Touch keyboards insert their newline before ProseMirror offers Enter to the keymap; | ||
| // claiming it makes ProseMirror discard that newline. A line break takes its place, | ||
| // since touch keyboards have no Shift-Enter. | ||
| addKeyboardShortcuts() { | ||
| return { Enter: () => this.editor.commands.setHardBreak() }; | ||
| }, |
There was a problem hiding this comment.
I know the issue used to say that line breaks were accepted, but I have reconsidered it, and given that we cannot have gaps with multi-line inputs, we should not have multi-line distractors either. Could you please handle this and make inlineOnly enforce a single-line editor? I have updated the issue.
There was a problem hiding this comment.
Done in 9ed37b5: inlineOnly is single-line.
hardBreakis out of the schema; Enter, Shift-Enter and Mod-Enter are claimed and insert nothing (touch-keyboard newline included).- Stored and pasted
<br>become a space; a trailing<br>is dropped. - Searched for other line-break paths: the toolbar paste and
toInlineHTMLboth go through the sameflattenBlocks, covered by tests; no toolbar offers a line break. - Capture: Add an inline-only mode to the rich text editor #6214 (comment)
There was a problem hiding this comment.
Two more single-line gaps fixed in a8d2bcd:
<hr>between text welded the two runs (a<hr>bsavedab); it now becomes a space like<br>.- Searched
TipTapEditor/foraria-multiline: 1 place (the container), hard-codedtrue. It and the ProseMirror element now sayfalsein inline-only mode. - Dropped
dropTrailingBreak/isEmptyand 9 paste tests that passed with flattening disabled; ProseMirror already strips the edge whitespace they covered.
01db6df to
9ed37b5
Compare
|
Single-line ./inline-single-line.webm The description's Enter/line-break rows predate this change and no longer apply. inline-single-line.webm |
61ec7a1 to
85ee9db
Compare
AlexVelezLl
left a comment
There was a problem hiding this comment.
A small finding from Claude which would be important to handle.
| editor.value.commands.focus(); | ||
| editor.value.view.pasteHTML(html); | ||
| } else { | ||
| editor.value.chain().focus().insertContent(transformPastedHTML(html)).run(); |
There was a problem hiding this comment.
suggestion: The inline-only branch now pastes through view.pasteHTML / view.pasteText, but the full-editor branch still uses insertContent, which skips ProseMirror's paste props: both transformPasted and the extension-level transformPastedHTML hooks that tiptap composes in editorProps. That predates this PR, but it is about to matter. #6204 gives a pasted gap a fresh identifier in transformPasted, and the gap match passage is a full editor, so pasting a gap from the toolbar would keep its identifier while Ctrl+V would not.
Does routing both branches through view.pasteHTML / view.pasteText change anything for the full editor? editorProps.transformPastedHTML already runs the same transformPastedHTML. If it's fine, the inlineOnly split here can go. If not, could this be noted on #6204 so the gap work handles toolbar paste too?
There was a problem hiding this comment.
Both branches now paste through view.pasteHTML / view.pasteText; the inlineOnly split is gone (f657695).
It does change the full editor, toward Ctrl+V parity:
- Extension
transformPastedHTMLand plugintransformPastednow run on toolbar paste. - Plain text:
insertContentparsed it as HTML, so Paste without formatting of<b>a</b>came out bold; it now stays text. - Plain-text lines become paragraphs instead of a
\ninside one text node. - Pasted HTML opens into the cursor's paragraph instead of appending new blocks.
Searched frontend/ for clipboard reads and pasteHTML/pasteText/insertContent paste paths: one helper pair serving both handlers (handlePaste, handlePasteNoFormat); both changed. Full-editor tests added in EditorToolbar.spec.js; screenshot in #6214 (comment).
85ee9db to
9145547
Compare
9145547 to
56689cb
Compare
- `initializeEditor(..., { inlineOnly })` leaves every block node and the line break out of the schema
- Enter, Shift-Enter and Mod-Enter insert nothing
- Pasted blocks and line breaks flatten to one space-separated inline run; plain-text lines join with spaces
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Stored block content loads flattened to inline and saves back inline - Dropped files are ignored in inline-only mode Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Toolbar paste runs the editor's and extensions' paste props, as Ctrl+V does - Inline-only editors join pasted blocks and lines with a space - Text pasted without formatting keeps its markup as text Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Desktop and mobile bars drop lists, image, code block, alignment and the format dropdown Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
56689cb to
11f0cf5
Compare
AlexVelezLl
left a comment
There was a problem hiding this comment.
Looks pretty polished, code changes look good to me! Thanks @rtibblesbot!



Summary
inlineOnlyprop builds the editor on a schema with no block nodes, for inline-only QTI fields such as gap match choices.References
Closes #6193. Builds on #6151.
Reviewer guidance
content: 'inline*':focus('end'),selectAll, Math node view.useEditor.jscancels Android's keydown-less Enterbeforeinput.aria-multiline="false"; output is validqti-gap-text.inlineOnlyto<TipTapEditor>inshared/views/QTIEditor/interactions/choice/Editor.vue.pnpm devsetupas admin: Published Channel → Topic 1 → Sample Exercise → Questions → Edit on Question 1.QA steps
<b>a</b>literal,**a**bold, Windows lines become paragraphs. Close, reload: unchanged from before Close.Evidence
Full editor, mobile formatting bar
mobile-bar-reopen-after-save.webm
Inline-only, typing
inline-typing-enter-variants-desktop.webm
Inline-only, paste
inline-toolbar-paste-and-undo.webm
Full editor copy, inline-only paste
s3-copy-full-paste-inline.webm
Inline-only, stored block content
s4-click-outside-and-minimize.webm
Full editor, toolbar paste
<b>a</b>literalfull-paste-toolbar-flow.webm
More captures (38)
AI usage
Used Claude Code to plan and implement the change test-first and to capture the screenshots. Verified with the TipTapEditor and QTIEditor Jest suites, pre-commit, and an axe audit in a headless browser.
🤖 Generated with Claude Code
Deviations from the issue spec
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
🟡 Waiting for feedback
Last updated: 2026-10-02 20:13 UTC