Skip to content

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

Open
rtibblesbot wants to merge 4 commits into
learningequality:unstablefrom
rtibblesbot:issue-6193-59cd5d
Open

rtibblesbot wants to merge 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.

References

Closes #6193. Builds on #6151.

Reviewer guidance

  • No field uses the prop yet.
  • Top node is content: 'inline*'; riskiest: focus('end'), selectAll, Math node view.
  • HTML only.
  • useEditor.js cancels Android's keydown-less Enter beforeinput.
  • aria-multiline is "false" inline-only, "true" otherwise.
  • Emitted values pass the QTI 3.0 XSD as qti-gap-text.

QA steps

Setup: pnpm devsetup admin. Q1: Published Channel → Topic 1 → Sample Exercise → Questions → Edit on Question 1. Inline-only: add inlineOnly to the choice <TipTapEditor> in shared/views/QTIEditor/interactions/choice/Editor.vue.

  1. Q1, desktop and mobile: every toolbar, More and insert item works and survives reopening.
  2. Q1 question field: Enter adds a paragraph, Shift-Enter a line; Paste and Paste without formatting keep both; after Edit it shows what was entered.
  3. Inline-only Q1, desktop: choice toolbar and More hold only inline tools.
  4. Inline-only Q1, mobile: no format size, list or align; insert menu has only math.
  5. Inline-only Q1, choice Alpha **bold** mid x² end, desktop or phone: Enter, Shift-Enter or Ctrl+Enter anywhere leaves that text unchanged; Minimize Toolbar shows it on one line.
  6. Inline-only Q1, choice Start **here** x² end: pasting rich HTML, a spreadsheet or multi-line text gives one single-spaced line, no image; one Ctrl+Z restores Start **here** x² end.
  7. Inline-only Q1: Copy the question field, Paste into a choice: its paragraphs on one line, one space apart.
  8. Without inline-only, save "Yellow" with two lines, a list and an image; inline-only Q1: "Yellow" is one line without markers or image, also after clicking outside or Minimize Toolbar.

Evidence

Inline-only, typing

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

Step Screenshot
Stored blocks shown as one line Stored blocks shown as one line
s4-click-outside-and-minimize.webm

Full editor, choice item

Step Screenshot
Blocks and line breaks kept after typing and pastes Blocks and line breaks kept after typing and pastes
More captures (39)
Step Screenshot
Inline-only: rich HTML pasted mid-text on one line Inline-only: rich HTML pasted mid-text on one line
Inline-only: paste undone in one step Inline-only: paste undone in one step
Inline-only: Paste options menu open Inline-only: Paste options menu open
Inline-only mobile: editing Inline-only mobile: editing
Ctrl+V from full editor into inline-only Ctrl+V from full editor into inline-only
Toolbar Copy, then toolbar Paste into inline-only Toolbar Copy, then toolbar Paste into inline-only
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 after Minimize Toolbar Stored block content after Minimize Toolbar
Full editor: question open with full toolbar Full editor: question open with full toolbar
Full editor: reopened, content preserved Full editor: reopened, content preserved
Full toolbar on a choice Full toolbar on a choice
Full editor mobile: formatting bar with all groups and dividers Full editor mobile: formatting bar with all groups and dividers
Full editor: Ctrl+V keeps heading, paragraphs and list as blocks Full editor: Ctrl+V keeps heading, paragraphs and list as blocks
Inline-only toolbar at full width Inline-only toolbar at full width
Inline-only More menu at 420px: inline tools only Inline-only More menu at 420px: inline tools only
Inline-only mobile: formatting bar without format size, lists or align Inline-only mobile: formatting bar without format size, lists or align
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 dropdown open Paste dropdown open
Paste as plain text Paste as plain text
Ctrl+V into a markdown-format editor Ctrl+V into a markdown-format editor
Inline-only toolbar at 820px Inline-only toolbar at 820px
Inline-only More menu at 320px Inline-only More menu at 320px
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: subscript applied Inline-only mobile: subscript 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
Inline-only toolbar at 420px Inline-only toolbar at 420px

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 (Mod-e, backticks), as in the full editor.
Toolbars offer only the listed tools Copy, paste, clear formatting and minimize stay; they create no blocks.

@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 05:35 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 2 times, most recently from a8d2bcd to 85fa44b Compare October 2, 2026 04:04
rtibblesbot and others added 4 commits October 1, 2026 22:34
- `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>
- Pasted blocks and lines join with a space, as on a native paste

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