Skip to content

Feature/ordered list and indent buttons#503

Merged
fccview merged 26 commits into
fccview:developfrom
reniko:feature/ordered-list-and-indent-buttons
May 18, 2026
Merged

Feature/ordered list and indent buttons#503
fccview merged 26 commits into
fccview:developfrom
reniko:feature/ordered-list-and-indent-buttons

Conversation

@reniko
Copy link
Copy Markdown
Contributor

@reniko reniko commented May 11, 2026

Summary

  • Ordered list toolbar button — added next to the existing bullet list button, using LeftToRightListNumberIcon. Supports rich-text and markdown mode. Keyboard shortcut hint: Ctrl/⌘+Shift+7.
  • Indent / Outdent toolbar buttons — TextIndentMoreIcon / TextIndentLessIcon. Indent is enabled whenever the cursor is in a list. Outdent is only enabled at nesting level 2+, so it never accidentally removes a top-level list.
  • Ordered list slash command — appears directly after the bullet list entry in the / menu.
  • Tab / Shift+Tab for lists — explicitly wired in KeyboardShortcuts: Tab indents, Shift+Tab outdents (level 2+ only; at level 1 the event is consumed without destroying the list).
  • Toggle bug fix (bullet + ordered list) — toggleBulletList / toggleOrderedList had a bug where clicking the button on an empty list item caused cursor jumps or duplicate symbols. Fixed by using liftListItem when the current item is empty, for both buttons.
  • Reactive toolbar state — replaced ad-hoc editor.isActive() calls in the toolbar with useEditorState (TipTap v3), so button states update correctly on mouse-click and arrow-key navigation.
  • i18n — all new UI strings added to all 13 translation files (toggleOrderedList, indentListItem, outdentListItem, createOrderedList).

Executed tests

  • Ordered list button creates/removes a numbered list (and vice versa)
  • Clicking the button on an empty list item removes the list cleanly (no cursor jump, no duplicate symbol) — bullet list too
  • Indent button active in any list; clicking on item 2+ indents it; on item 1 (no previous sibling) nothing happens
  • Outdent button disabled at level 1, active at level 2+ (both lists)
  • Tab / Shift+Tab indent/outdent in both lists; Shift+Tab at level 1 does nothing
  • Table Tab/Shift+Tab navigation unchanged
  • /ordered in slash menu inserts a numbered list
  • Toolbar button states update when navigating with mouse or arrow keys
  • yarn lint passes

🤖 Generated with Claude Code.

Manual review and tests done. But as generated in assistance with claude code and with limited understanding of the complete architecture maybe please be careful ;-)

…buttons

Adds editor.toggleOrderedList, editor.indentListItem, editor.outdentListItem
and editor.createOrderedList to all 13 translation files.
Adds three buttons after the existing bulletList button:
- orderedList (LeftToRightListNumberIcon, Ctrl+Shift+7, dual-mode aware)
- indent/sinkListItem (TextIndentMoreIcon, disabled when not in a list)
- outdent/liftListItem (TextIndentLessIcon, disabled when not in a list)
Inserts an orderedList item directly after bulletList in the slash command
list, using LeftToRightListNumberIcon and the existing toggleOrderedList command.
- Indent: was disabled for first list item (can().sinkListItem returns false
  without a previous sibling). Changed to isActive('listItem') so the button
  stays visible; clicking on an unsinkable item is a no-op but less confusing
  than a grayed-out button.
- Outdent: was enabled on top-level list items, causing liftListItem to remove
  the list entirely on click. Added a ProseMirror depth check so outdent is
  only enabled when the item is nested (grandgrandparent node is a listItem).
- Ordered list button: bypass handleButtonClick wrapper for rich-text mode.
  The wrapper saved/restored cursor positions (from/to) around the command,
  but toggleOrderedList changes document structure so the saved positions
  become stale, causing the next click to land outside the list and create
  a new one instead of toggling. Now calls chain().focus() directly.
- Indent disabled: changed from isActive('listItem') (checks current block
  type = paragraph, always false) to isActive('bulletList')||isActive('orderedList').
- Outdent disabled: same reliable list-active check, depth condition unchanged.
TipTap's built-in ListItem Tab handler wasn't reliably firing after the
custom extension returned false. Now explicitly:
- Tab in ordered/bullet list: sinkListItem (indent), event always consumed
  to prevent browser focus-jump even if the item has no previous sibling.
- Shift-Tab in list: liftListItem only when nested (depth check); at level 1
  the event is consumed but the list is preserved.
- Import useEditorState (TipTap v3) to subscribe to selection changes;
  the toolbar now re-renders when the cursor moves in/out of a list, fixing
  the stale disabled state on mouse-click and arrow-key navigation.
- Selector computes isInList, isNested (ancestor traversal, no fixed depth
  offset), isInOrderedList, and currentItemIsEmpty — all driven by the
  live editor state.
- Ordered list toggle: when already in an ordered list with an empty item,
  use liftListItem instead of toggleOrderedList, which avoids the "cursor
  jumps / extra 1. inserted" bug that occurs on empty paragraphs.
- Outdent disabled: replaced editor.state.selection.$anchor.node(depth-3)
  fixed-offset heuristic with a proper ancestor walk through listItem nodes,
  which works correctly regardless of document nesting depth.
Same root cause as the ordered list fix: toggleBulletList on an empty list
item caused cursor jumps or extra bullet symbols. Now uses liftListItem when
the current item is empty, consistent with the ordered list behaviour.
Also adds isInBulletList to the useEditorState selector so the button's
active variant is driven by reactive state rather than a stale isActive call.
@fccview
Copy link
Copy Markdown
Owner

fccview commented May 12, 2026

Hey @reniko

This works mostly perfectly, however when you are in markdown mode the whole thing is very finnicky, you may want to double check the functionality there, especially the indent one is doing nothing (the numbered list works fine, but it's not smart enough to understand a list already has a number (e.g. if i click it whilst selecting 1. item it'll turn it to 1. 1. item

I also wonder if we should have bullet point menus being a dropdown instead (bit like the code block one) as otherwise half of the toolbar now is bullet point buttons :)

reniko and others added 17 commits May 12, 2026 18:25
Turndown serialises bullet items as '-   ' (dash + 3 spaces) and ordered
items as 'N.  ' (number + dot + 2 spaces), placing text at column 4.
The insertBulletList and insertOrderedList helpers used single-space
variants, causing visual inconsistency after a RT→MD roundtrip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pressing Enter on a numbered list line ('1. text') now inserts a new
line with the incremented number and same spacing ('2.  '). Pressing
Enter on an empty numbered item ('1.  ') removes the marker, consistent
with bullet list behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two text-based helpers in markdown-editor-utils that operate on all
selected lines (or the cursor line when nothing is selected).
indentLines prepends 4 spaces; outdentLines removes the first 4 spaces
if present, otherwise leaves the line unchanged — consistent with the
library's Shift+Tab outdent behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Indent and outdent buttons now route to the text-based indentLines/
outdentLines helpers in MD mode instead of calling TipTap commands that
have no effect there. The disabled condition is also corrected: buttons
are always enabled in MD mode (outdent on a line with <4 spaces is a
no-op, consistent with Shift+Tab).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New key for the list menu dropdown trigger button label, added to
en, de, es, fr, it, ko, nl, pl, ru, tr, zh, klingon, and pirate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bullet list, ordered list, indent and outdent are now grouped under a
single dropdown in the toolbar, following the same pattern as
CodeBlocksDropdown. The dropdown handles both rich-text (TipTap
commands) and MD mode (text-based helpers), and shows an active state
when the cursor is inside a list in rich-text mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleBulletListEnter and handleOrderedListEnter matched only at the
line start, so pressing Enter on a nested/indented item (e.g.
'    -   sub') inserted a plain newline instead of continuing the list.
Regex now captures leading whitespace and includes it in the new line
prefix so all indent levels work correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… mode

Replaced isInList/isNested with reactive editor.can().sinkListItem() and
editor.can().liftListItem() calls so the dropdown items are disabled
precisely when TipTap cannot execute the command — e.g. indent is now
correctly disabled on the first item of a list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
remark does not reliably parse nested ordered lists starting at numbers
other than 1 (a CommonMark edge case where the 'cannot interrupt
paragraph' rule is over-applied). To ensure the MD→RT roundtrip is
always correct, indentLines now renumbers ordered list items in the
selection starting from 1 when they are indented. Each contiguous group
of ordered items gets its own counter that resets at blank lines and
non-ordered lines.

Also fixes the RT indent disabled state: uses editor.can().sinkListItem
and editor.can().liftListItem so the dropdown items reflect the exact
command availability, e.g. indent is disabled on the first item of a
list level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r after indent

Two fixes in markdown-editor-utils:

1. insertBulletList / insertOrderedList now add list markers to empty
   lines when toggling on, so clicking the list button on a blank line
   produces a list item. When toggling off (allMatch), empty lines are
   left unchanged as before.

2. indentLines / outdentLines collapse the selection to a cursor after
   the operation. processLineSelection selected the full modified block
   which caused the next keystroke to delete all indented content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
processLineSelection treated empty lines as 'matching' the pattern so
an all-empty selection produced allMatch=true, sending the callback into
toggle-off mode instead of toggle-on. Added a hasNonEmpty guard so
allMatch is only true when at least one non-empty line actually matches.

Result: clicking the ordered list button (or bullet list) on a blank
line now inserts a list marker, consistent with non-empty lines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add liftOutOfList() loop in ListMenuDropdown that repeatedly calls
liftListItem until the cursor is no longer inside any list, instead of
lifting only one level. Also enables cross-type switching (bullet ↔
ordered) without first exiting the list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e switching

Rewrite insertBulletList and insertOrderedList to use /^\s*-\s/ and
/^\s*\d+\.\s/ patterns so indented list items are recognized. When all
selected lines are already a list of the other type, convert markers
instead of stacking a new marker on top.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The paragraphInLi Turndown rule only stripped paragraph block-wrapping
when P was the sole child of LI. Lifting that restriction ensures P
elements inside LI are inlined even when a nested list is a sibling,
preventing a blank line from appearing before the nested list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add getListDepth() to count listItem ancestors and switchListType() that
lifts the item all the way out, toggles to the new list type, then sinks
back to the original depth. This fixes the bug where switching bullet↔
ordered on a nested item always moved it to level 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detect the innermost list type with a ProseMirror ancestor walk instead
of listState flags, which are both true for nested lists. When the
clicked type differs from the innermost list, call toggleBulletList/
toggleOrderedList directly — their internal toggleList implementation
runs setNodeMarkup on the immediate parent list and preserves nesting
depth. Same-type clicks still call liftOutOfList for a full exit.

This restores the ability to switch a level-2 bullet to a level-2
ordered list (and vice versa) without flattening to level 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@reniko
Copy link
Copy Markdown
Contributor Author

reniko commented May 15, 2026

Ok, second try finished. I tried my best with Claude but changes started getting a little complex. Could you please check?

New Features

Ordered list support (initial)

  • Added ordered list, indent, and outdent toolbar buttons with translation keys for all 13 locales
  • Added an ordered list entry to the slash commands menu
  • Added ordered list continuation on Enter in Markdown mode (pressing Enter on 1. item creates 2. )
  • Added indentLines / outdentLines helpers for Markdown mode (adds/removes 4-space indentation)
    ``

Toolbar UX improvement

  • Replaced the four individual list/indent toolbar buttons (bullet list, ordered list, indent, outdent) with a single ListMenuDropdown — a compact dropdown showing all four actions, reducing toolbar clutter

i18n

Added translation keys editor.listOptions, editor.toggleOrderedList, editor.indentListItem, editor.outdentListItem to all 13 locale files

Bug Fixes (post-review)

Markdown mode

  • Aligned bullet (- ) and ordered (N. ) list markers to match Turndown's convention, ensuring clean round-trips from Rich Text → Markdown
  • Fixed Enter continuation for indented list items (e.g. - item) — the regex previously required the marker at column 0
  • Fixed insertBulletList and insertOrderedList to use indent-aware patterns (/^\s*-\s/, /^\s*\d+.\s/) so buttons correctly detect and toggle list items that are indented (nested)
  • Fixed type switching in Markdown: clicking bullet on an ordered list now replaces ordered markers with bullet markers (and vice versa), instead of stacking a second marker
  • Fixed empty-line toggle-on for both bullet and ordered lists (the allMatch guard was incorrectly treating all-empty selections as "already a list")
  • Fixed cursor selection after indent/outdent (previously the entire block was selected, causing the next keystroke to delete everything)

Rich Text mode

  • Fixed can()-based disabled state for indent/outdent buttons (previously always enabled or always disabled)
  • Fixed ordered list renumbering on indent so nested ordered lists always start at 1. (avoids a remark/CommonMark parsing limitation with lists starting above 1)
  • Fixed list toggle to exit all nesting levels, not just one — clicking the active list button now removes the item from the list entirely regardless of depth
  • Fixed list type switching at nested levels — clicking the other list type on a level-2 item now changes that item's list type in-place (via TipTap's setNodeMarkup), preserving the nesting depth instead of flattening to level 1

Rich Text → Markdown conversion

  • Fixed extra blank lines appearing before nested lists when converting from Rich Text to Markdown — the Turndown paragraphInLi rule was not stripping block-level wrapping from <p> elements inside <li> when a nested list was also present

@fccview
Copy link
Copy Markdown
Owner

fccview commented May 18, 2026

Good job to you and the robots, it's clean and works well, merged!

@fccview fccview merged commit f9c7e66 into fccview:develop May 18, 2026
2 checks passed
@fccview fccview mentioned this pull request May 18, 2026
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.

3 participants