From f7bb85b3c694b71920b8477e6a6c674aae402aab Mon Sep 17 00:00:00 2001 From: Anton Bolshakov Date: Sat, 13 Jun 2026 08:13:07 +0800 Subject: [PATCH 1/2] Fix increaseNestingLevel/decreaseNestingLevel for multi-item selections Both methods called getBlock() which returns only the first block in the selection (locationRange[0].index). When multiple list items were selected, only the first item's nesting level changed. Fix: iterate over every block index from locationRange[0].index to locationRange[1].index, chaining replaceBlock() calls through the updated document so all selected items are adjusted atomically before setDocument() is called. Add regression tests that select three items and assert all three change level. --- src/test/system/list_formatting_test.js | 32 +++++++++++++++++++++++++ src/trix/models/composition.js | 22 ++++++++++++----- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/test/system/list_formatting_test.js b/src/test/system/list_formatting_test.js index 3d3ab7c05..1196e9920 100644 --- a/src/test/system/list_formatting_test.js +++ b/src/test/system/list_formatting_test.js @@ -96,6 +96,38 @@ testGroup("List formatting", { template: "editor_empty" }, () => { expectDocument("ab\nc\n") }) + test("increasing nesting level applies to all selected items", async () => { + await clickToolbarButton({ attribute: "bullet" }) + await typeCharacters("a\nb\nc") + getSelectionManager().setLocationRange([ + { index: 0, offset: 0 }, + { index: 2, offset: 1 }, + ]) + await clickToolbarButton({ action: "increaseNestingLevel" }) + assert.blockAttributes([ 0, 2 ], [ "bulletList", "bullet", "bulletList", "bullet" ]) + assert.blockAttributes([ 2, 4 ], [ "bulletList", "bullet", "bulletList", "bullet" ]) + assert.blockAttributes([ 4, 6 ], [ "bulletList", "bullet", "bulletList", "bullet" ]) + expectDocument("a\nb\nc\n") + }) + + test("decreasing nesting level applies to all selected items", async () => { + await clickToolbarButton({ attribute: "bullet" }) + await typeCharacters("a\n") + await clickToolbarButton({ action: "increaseNestingLevel" }) + await typeCharacters("b\n") + await clickToolbarButton({ action: "increaseNestingLevel" }) + await typeCharacters("c") + getSelectionManager().setLocationRange([ + { index: 1, offset: 0 }, + { index: 2, offset: 1 }, + ]) + await clickToolbarButton({ action: "decreaseNestingLevel" }) + assert.blockAttributes([ 0, 2 ], [ "bulletList", "bullet" ]) + assert.blockAttributes([ 2, 4 ], [ "bulletList", "bullet" ]) + assert.blockAttributes([ 4, 6 ], [ "bulletList", "bullet" ]) + expectDocument("a\nb\nc\n") + }) + test("decreasing list item's level decreases its nested items level too", async () => { await clickToolbarButton({ attribute: "bullet" }) await typeCharacters("a\n") diff --git a/src/trix/models/composition.js b/src/trix/models/composition.js index ad61bb358..b7269b7cc 100644 --- a/src/trix/models/composition.js +++ b/src/trix/models/composition.js @@ -415,15 +415,25 @@ export default class Composition extends BasicObject { } decreaseNestingLevel() { - const block = this.getBlock() - if (!block) return - return this.setDocument(this.document.replaceBlock(block, block.decreaseNestingLevel())) + const locationRange = this.getLocationRange() + if (!locationRange) return + let document = this.document + for (let i = locationRange[0].index; i <= locationRange[1].index; i++) { + const block = document.getBlockAtIndex(i) + if (block) document = document.replaceBlock(block, block.decreaseNestingLevel()) + } + return this.setDocument(document) } increaseNestingLevel() { - const block = this.getBlock() - if (!block) return - return this.setDocument(this.document.replaceBlock(block, block.increaseNestingLevel())) + const locationRange = this.getLocationRange() + if (!locationRange) return + let document = this.document + for (let i = locationRange[0].index; i <= locationRange[1].index; i++) { + const block = document.getBlockAtIndex(i) + if (block) document = document.replaceBlock(block, block.increaseNestingLevel()) + } + return this.setDocument(document) } canDecreaseBlockAttributeLevel() { From 0e6691290b307971a408cf6797eadfee0ba0b583 Mon Sep 17 00:00:00 2001 From: Anton Bolshakov Date: Sat, 13 Jun 2026 08:42:48 +0800 Subject: [PATCH 2/2] Address review feedback: offset-0 end guard and correct test assertion - Skip the end block when selection ends at offset 0 (caret at block start means that block is not covered by the selection). Applied to both increaseNestingLevel and decreaseNestingLevel. - Fix decrease-nesting test: item c starts at level 2, one decreaseNestingLevel call brings it to level 1, not level 0. Corrected expected blockAttributes. --- src/test/system/list_formatting_test.js | 2 +- src/trix/models/composition.js | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/test/system/list_formatting_test.js b/src/test/system/list_formatting_test.js index 1196e9920..749110548 100644 --- a/src/test/system/list_formatting_test.js +++ b/src/test/system/list_formatting_test.js @@ -124,7 +124,7 @@ testGroup("List formatting", { template: "editor_empty" }, () => { await clickToolbarButton({ action: "decreaseNestingLevel" }) assert.blockAttributes([ 0, 2 ], [ "bulletList", "bullet" ]) assert.blockAttributes([ 2, 4 ], [ "bulletList", "bullet" ]) - assert.blockAttributes([ 4, 6 ], [ "bulletList", "bullet" ]) + assert.blockAttributes([ 4, 6 ], [ "bulletList", "bullet", "bulletList", "bullet" ]) expectDocument("a\nb\nc\n") }) diff --git a/src/trix/models/composition.js b/src/trix/models/composition.js index b7269b7cc..1bc760a5d 100644 --- a/src/trix/models/composition.js +++ b/src/trix/models/composition.js @@ -417,8 +417,12 @@ export default class Composition extends BasicObject { decreaseNestingLevel() { const locationRange = this.getLocationRange() if (!locationRange) return + const startIndex = locationRange[0].index + const endIndex = locationRange[1].offset === 0 && locationRange[1].index > startIndex + ? locationRange[1].index - 1 + : locationRange[1].index let document = this.document - for (let i = locationRange[0].index; i <= locationRange[1].index; i++) { + for (let i = startIndex; i <= endIndex; i++) { const block = document.getBlockAtIndex(i) if (block) document = document.replaceBlock(block, block.decreaseNestingLevel()) } @@ -428,8 +432,12 @@ export default class Composition extends BasicObject { increaseNestingLevel() { const locationRange = this.getLocationRange() if (!locationRange) return + const startIndex = locationRange[0].index + const endIndex = locationRange[1].offset === 0 && locationRange[1].index > startIndex + ? locationRange[1].index - 1 + : locationRange[1].index let document = this.document - for (let i = locationRange[0].index; i <= locationRange[1].index; i++) { + for (let i = startIndex; i <= endIndex; i++) { const block = document.getBlockAtIndex(i) if (block) document = document.replaceBlock(block, block.increaseNestingLevel()) }