Skip to content

Add an inline-only mode to the rich text editor - #6214

Merged
AlexVelezLl merged 4 commits into
learningequality:unstablefrom
rtibblesbot:issue-6193-59cd5d
Oct 2, 2026
Merged

AlexVelezLl merged 4 commits into
learningequality:unstablefrom
rtibblesbot:issue-6193-59cd5d

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • inlineOnly prop builds the editor on a schema with no block nodes, for inline-only QTI fields such as gap match choices.
  • Toolbars, mobile bars, shortcuts and input rules offer only inline tools.
  • Block content from storage or the clipboard flattens to one inline run; images are dropped.
  • Toolbar Paste / Paste without formatting go through ProseMirror in every editor, so they match Ctrl+V / Ctrl+Shift+V.

References

Closes #6193. Builds on #6151.

Reviewer guidance

  • No field uses the prop yet; HTML only.
  • Riskiest with content: 'inline*': focus('end'), selectAll, Math node view.
  • useEditor.js cancels Android's keydown-less Enter beforeinput.
  • Inline-only sets aria-multiline="false"; output is valid qti-gap-text.
  • Inline-only: add inlineOnly to <TipTapEditor> in shared/views/QTIEditor/interactions/choice/Editor.vue.
  • Q1, after pnpm devsetup as admin: Published Channel → Topic 1 → Sample Exercise → Questions → Edit on Question 1.

QA steps

  1. Q1, desktop and mobile: every toolbar, More and insert item works.
  2. Q1 Question: Enter adds a paragraph, Shift-Enter a line break.
  3. Inline-only Q1 choice: toolbar and More show only inline tools.
  4. Inline-only Q1 choice: Enter, Shift-Enter, Ctrl+Enter do nothing.
  5. Inline-only Q1 choice: pasted HTML or multi-line text lands on one line, no image.
  6. Inline-only Q1: Copy the question, Paste into a choice: one line.
  7. Without inline-only, save choice "Yellow" with a list and image; with it: one line, no image.
  8. Published Channel → Topic 2 → Add → New exercise → Questions → New question → Question: Paste Word HTML: Ctrl+V's blocks, no images; one Ctrl+Z undoes it. Paste without formatting: <b>a</b> literal, **a** bold, Windows lines become paragraphs. Close, reload: unchanged from before Close.
  9. At 412×915 touch, Published Channel → Sample Exercise → Edit details → QUESTIONS → Edit on Question 1 of 4 → Question prompt → Expand formatting bar: one divider between groups. Strong, Italic, Underline, Strikethrough, Numbered list, Bullet list, Align right format the selection and show pressed (Align right becomes Align left). Reload, reopen the prompt: same content and pressed states as before reload.

Evidence

Full editor, mobile formatting bar

Step Screenshot
Bar expanded: one divider between groups Bar expanded: one divider between groups
Formatting applied, buttons pressed Formatting applied, buttons pressed
mobile-bar-reopen-after-save.webm

Inline-only, typing

Step Screenshot
Before the fix: Android Enter beside x² adds a space Before the fix: Android Enter beside x² adds a space
After: "mid x² end" unchanged After: "mid x² end" unchanged
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

Step Screenshot
Word HTML on an empty line: own blocks Word HTML on an empty line: own blocks
Paste without formatting: <b>a</b> literal Paste without formatting: literal tags
full-paste-toolbar-flow.webm
More captures (38)
Step Screenshot
Full: before mid-text paste Full: before mid-text paste
Full: Paste mid-text Full: Paste mid-text
Full: one Ctrl+Z Full: one Ctrl+Z
Full: answer typed Full: answer typed
Full: reopened Full: reopened
Inline-only: stored blocks on one line Inline-only: stored blocks on one line
Full: blocks and line breaks kept Full: blocks and line breaks kept
Inline-only: rich HTML paste, one line Inline-only: rich HTML paste, one line
Inline-only: paste undone Inline-only: paste undone
Inline-only: paste menu Inline-only: paste menu
Inline-only mobile: editing Inline-only mobile: editing
Full to inline-only Ctrl+V Full to inline-only Ctrl+V
Full to inline-only toolbar paste Full to inline-only toolbar paste
Inline-only after paste, minimized Inline-only after paste, minimized
Stored block content in edit mode Stored block content in edit mode
Stored block content, minimized Stored block content, minimized
Full toolbar on a choice Full toolbar on a choice
Full: Ctrl+V keeps blocks Full: Ctrl+V keeps blocks
Inline-only toolbar at full width Inline-only toolbar at full width
Inline-only More menu, 420px Inline-only More menu, 420px
Inline-only mobile bar Inline-only mobile bar
Full editor: math modal Full editor: math modal
Full editor mobile: insert menu Full editor mobile: insert menu
Full editor mobile: formatted stem Full editor mobile: formatted stem
Paste as plain text Paste as plain text
Ctrl+V into markdown editor Ctrl+V into markdown editor
Inline-only marks applied Inline-only marks applied
Inline-only math modal Inline-only math modal
Inline-only math inserted, minimized Inline-only math inserted, minimized
Inline-only mobile: insert menu with math only Inline-only mobile: insert menu with math only
Inline-only mobile: bold applied Inline-only mobile: bold applied
Inline-only mobile: superscript applied Inline-only mobile: superscript applied
Inline-only mobile: math editor Inline-only mobile: math editor
Inline-only mobile: math inserted Inline-only mobile: math inserted
Inline-only mobile: minimized Inline-only mobile: minimized
Backticks make inline code Backticks make inline code
Markdown rules create no blocks Markdown rules create no blocks
Emptied editor, minimized Emptied editor, minimized

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

Criterion Why not
Toolbars offer inline code Keyboard-only, as in the full editor.
Toolbars offer only the listed tools Copy, paste, clear formatting, minimize kept.
Fields without inline-only unchanged Toolbar paste matches Ctrl+V (AlexVelezLl).

@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks
@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-10-02 20:13 UTC

@rtibblesbot
rtibblesbot marked this pull request as ready for review September 30, 2026 14:20
@rtibblesbot
rtibblesbot requested review from a team and marcellamaki September 30, 2026 14:20
@rtibblesbot
rtibblesbot force-pushed the issue-6193-59cd5d branch 3 times, most recently from 39ba7fd to 01db6df Compare October 1, 2026 08:31

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, the clipboardSerializer override); all removed.
  • trimSourceEdges, collapseWhitespace, collapseCodeWhitespace and 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.

Comment on lines +17 to +18
// 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not mention qti-gap-text here; it should know nothing about the QTI editor. This should be general.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +15
// 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() };
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9ed37b5: inlineOnly is single-line.

  • hardBreak is 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 toInlineHTML both go through the same flattenBlocks, covered by tests; no toolbar offers a line break.
  • Capture: Add an inline-only mode to the rich text editor #6214 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two more single-line gaps fixed in a8d2bcd:

  • <hr> between text welded the two runs (a<hr>b saved ab); it now becomes a space like <br>.
  • Searched TipTapEditor/ for aria-multiline: 1 place (the container), hard-coded true. It and the ProseMirror element now say false in inline-only mode.
  • Dropped dropTrailingBreak/isEmpty and 9 paste tests that passed with flattening disabled; ProseMirror already strips the edge whitespace they covered.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Single-line inlineOnly editor (harness page; no app field uses inlineOnly yet):

  • Enter, Shift-Enter and Mod-Enter insert nothing:
    Enter inserts nothing
  • A pasted multi-line block lands as one line:
    Pasted lines joined by spaces

./inline-single-line.webm

The description's Enter/line-break rows predate this change and no longer apply.

inline-single-line.webm

@rtibblesbot
rtibblesbot force-pushed the issue-6193-59cd5d branch 3 times, most recently from 61ec7a1 to 85ee9db Compare October 2, 2026 05:34

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 transformPastedHTML and plugin transformPasted now run on toolbar paste.
  • Plain text: insertContent parsed 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 \n inside 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).

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Full editor, Q1 question field: toolbar Paste without formatting of <b>Not bold</b> & plain + newline + Second line keeps the markup as text and pastes each line as a paragraph, as Ctrl+Shift+V does. Before this change the toolbar bolded it and kept the newline inside one paragraph.

Full editor after toolbar Paste without formatting

rtibblesbot and others added 4 commits October 2, 2026 13:10
- `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>

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty polished, code changes look good to me! Thanks @rtibblesbot!

@AlexVelezLl
AlexVelezLl merged commit ec6eb52 into learningequality:unstable Oct 2, 2026
13 checks passed
@rtibblesbot
rtibblesbot deleted the issue-6193-59cd5d branch October 2, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QTI] Add an inline-only mode to TipTapEditor for gap match choices

2 participants