Project

General

Profile

Feature #44061 » 0002-Extend-Tab-Shift-Tab-indentation-from-list-items-to-.patch

Mizuki ISHIKAWA, 2026-05-29 06:31

View differences:

app/helpers/application_helper.rb
1433 1433
    return {} if Setting.text_formatting.blank?
1434 1434

  
1435 1435
    {
1436
      controller: 'list-autofill list-indent table-paste',
1437
      action: 'beforeinput->list-autofill#handleBeforeInput keydown.tab->list-indent#run keydown.shift+tab->list-indent#run paste->table-paste#handlePaste',
1436
      controller: 'list-autofill selection-indent table-paste',
1437
      action: 'beforeinput->list-autofill#handleBeforeInput keydown.tab->selection-indent#run keydown.shift+tab->selection-indent#run paste->table-paste#handlePaste',
1438 1438
      list_autofill_text_formatting_param: Setting.text_formatting,
1439
      list_indent_text_formatting_param: Setting.text_formatting,
1439
      selection_indent_text_formatting_param: Setting.text_formatting,
1440 1440
      table_paste_text_formatting_param: Setting.text_formatting
1441 1441
    }
1442 1442
  end
app/javascript/controllers/list_indent_controller.js → app/javascript/controllers/selection_indent_controller.js
1 1
import { Controller } from '@hotwired/stimulus'
2 2

  
3 3
export default class extends Controller {
4
  #bulletPattern = /^\s*[*+\-] /
5
  #orderedPattern = /^\s*\d+[.)] /
4
  #spaces = 2
6 5

  
7 6
  run(event) {
8 7
    const format = event.params.textFormatting
......
17 16
    const endPos = end === -1 ? value.length : end
18 17
    const selectedText = value.slice(start, endPos)
19 18
    const lines = selectedText.split("\n")
20
    const spaces = this.#indentSize(lines.find(l => this.#indentSize(l)) || "")
21
    if (!spaces) return
22 19

  
23 20
    event.preventDefault()
24 21

  
25 22
    const newLines = event.shiftKey
26
      ? lines.map(line => this.#unindentLine(line, spaces))
27
      : lines.map(line => this.#indentLine(line, spaces))
23
      ? lines.map(line => this.#unindentLine(line))
24
      : lines.map(line => this.#indentLine(line))
28 25

  
29 26
    const newText = newLines.join("\n")
30 27

  
31 28
    input.setRangeText(newText, start, endPos, "preserve")
29
    input.setSelectionRange(
30
      Math.max(start, selectionStart + newLines[0].length - lines[0].length),
31
      selectionEnd + newText.length - selectedText.length
32
    )
32 33
  }
33 34

  
34
  #indentSize(line) {
35
    if (this.#bulletPattern.test(line)) return 2
36
    if (this.#orderedPattern.test(line)) return 4
37
    return 0
35
  #indentLine(line) {
36
    return " ".repeat(this.#spaces) + line
38 37
  }
39 38

  
40
  #indentLine(line, spaces) {
41
    if (!this.#indentSize(line)) return line
42
    return " ".repeat(spaces) + line
43
  }
44

  
45
  #unindentLine(line, spaces) {
39
  #unindentLine(line) {
46 40
    const currentIndent = line.match(/^(\s*)/)[1].length
47
    const remove = Math.min(spaces, currentIndent)
41
    const remove = Math.min(this.#spaces, currentIndent)
48 42
    return line.slice(remove)
49 43
  }
50

  
51 44
}
test/system/list_indent_test.rb → test/system/selection_indent_test.rb
2 2

  
3 3
require_relative '../application_system_test_case'
4 4

  
5
class ListIndentSystemTest < ApplicationSystemTestCase
5
class SelectionIndentSystemTest < ApplicationSystemTestCase
6 6
  def setup
7 7
    super
8 8
    log_user('jsmith', 'jsmith')
9 9
  end
10 10

  
11
  # Tab: inserts 2 spaces for bullet lists, 4 spaces for ordered lists
12
  def test_tab_indents_common_mark_bullet_list
11
  def test_tab_indents_selected_text
13 12
    with_settings :text_formatting => 'common_mark' do
14 13
      visit '/projects/ecookbook/issues/new'
15 14

  
16 15
      within('form#issue-form') do
16
        text = "hello"
17 17
        el = find('#issue_description')
18 18
        el.click
19
        set_textarea_value 'issue_description', "- item", selection: [0, text.length]
19
        set_textarea_value 'issue_description', text, selection: [0, text.length]
20 20
        el.send_keys(:tab)
21
        assert_equal "  - item", el.value
21
        assert_equal "  hello", el.value
22 22
      end
23 23
    end
24 24
  end
25 25

  
26
  def test_tab_indents_common_mark_ordered_list
26
  def test_tab_indents_multiple_selected_lines
27 27
    with_settings :text_formatting => 'common_mark' do
28 28
      visit '/projects/ecookbook/issues/new'
29 29

  
30 30
      within('form#issue-form') do
31
        text = "hello\nworld"
31 32
        el = find('#issue_description')
32 33
        el.click
33
        set_textarea_value 'issue_description', "1. item", selection: [0, text.length]
34
        set_textarea_value 'issue_description', text, selection: [0, text.length]
34 35
        el.send_keys(:tab)
35
        assert_equal "    1. item", el.value
36
        assert_equal "  hello\n  world", el.value
36 37
      end
37 38
    end
38 39
  end
39 40

  
40
  # Shift+Tab: removes indentation
41
  def test_shift_tab_unindents_common_mark_bullet_list
41
  def test_tab_indents_only_lines_within_selection
42 42
    with_settings :text_formatting => 'common_mark' do
43 43
      visit '/projects/ecookbook/issues/new'
44 44

  
45 45
      within('form#issue-form') do
46
        text = "line1\nline2\nline3"
47
        start = text.index("line2")
46 48
        el = find('#issue_description')
47 49
        el.click
48
        set_textarea_value 'issue_description', "- parent\n  - child", selection: [0, text.length]
49
        el.send_keys([:shift, :tab])
50
        assert_equal "- parent\n- child", el.value
51
      end
52
    end
53
  end
54

  
55
  def test_shift_tab_removes_partial_indent_on_common_mark_list
56
    # Removes only as many spaces as exist when indent is less than the step size
57
    with_settings :text_formatting => 'common_mark' do
58
      visit '/projects/ecookbook/issues/new'
59

  
60
      within('form#issue-form') do
61
        el = find('#issue_description')
62
        el.click
63
        set_textarea_value 'issue_description', "- parent\n - child", selection: [0, text.length]
64
        el.send_keys([:shift, :tab])
65
        assert_equal "- parent\n- child", el.value
66
      end
67
    end
68
  end
69

  
70
  def test_shift_tab_does_nothing_on_common_mark_list_with_no_indent
71
    with_settings :text_formatting => 'common_mark' do
72
      visit '/projects/ecookbook/issues/new'
73

  
74
      within('form#issue-form') do
75
        el = find('#issue_description')
76
        el.click
77
        set_textarea_value 'issue_description', "- item", selection: [0, text.length]
78
        el.send_keys([:shift, :tab])
79
        assert_equal "- item", el.value
50
        set_textarea_value 'issue_description', text, selection: [start, start + "line2".length]
51
        el.send_keys(:tab)
52
        assert_equal "line1\n  line2\nline3", el.value
80 53
      end
81 54
    end
82 55
  end
83 56

  
84
  def test_tab_indents_multiple_lines_when_selected
57
  def test_tab_does_not_indent_without_selection
85 58
    with_settings :text_formatting => 'common_mark' do
86 59
      visit '/projects/ecookbook/issues/new'
87 60

  
88 61
      within('form#issue-form') do
62
        fill_in 'Description', with: "hello"
89 63
        el = find('#issue_description')
90 64
        el.click
91
        set_textarea_value 'issue_description', "- item1\n- item2", selection: [0, text.length]
92 65
        el.send_keys(:tab)
93
        assert_equal "  - item1\n  - item2", el.value
66
        assert_equal "hello", el.value
94 67
      end
95 68
    end
96 69
  end
97 70

  
98
  def test_tab_does_not_indent_without_list_or_selection
71
  def test_shift_tab_unindents_selected_text
99 72
    with_settings :text_formatting => 'common_mark' do
100 73
      visit '/projects/ecookbook/issues/new'
101 74

  
102 75
      within('form#issue-form') do
103
        fill_in 'Description', with: "normal text"
76
        text = "  hello\n  world"
104 77
        el = find('#issue_description')
105 78
        el.click
106
        el.send_keys(:tab)
107
        assert_equal "normal text", el.value
79
        set_textarea_value 'issue_description', text, selection: [0, text.length]
80
        el.send_keys([:shift, :tab])
81
        assert_equal "hello\nworld", el.value
108 82
      end
109 83
    end
110 84
  end
111 85

  
112
  def test_tab_indents_only_selected_lines_block
86
  def test_shift_tab_removes_partial_indent
87
    # Removes only as many spaces as exist when indent is less than the step size
113 88
    with_settings :text_formatting => 'common_mark' do
114 89
      visit '/projects/ecookbook/issues/new'
115 90

  
116 91
      within('form#issue-form') do
117
        text = "- item1\n- item2\n- item3"
118
        start = text.index("- item2")
119
        finish = start + "- item2".length
92
        text = "  hello\n world"
120 93
        el = find('#issue_description')
121 94
        el.click
122
        set_textarea_value 'issue_description', text, selection: [start, finish]
123
        el.send_keys(:tab)
124
        assert_equal "- item1\n  - item2\n- item3", el.value
95
        set_textarea_value 'issue_description', text, selection: [0, text.length]
96
        el.send_keys([:shift, :tab])
97
        assert_equal "hello\nworld", el.value
125 98
      end
126 99
    end
127 100
  end
128 101

  
129
  def test_tab_indents_only_selected_lines_block_ordered_list
102
  def test_shift_tab_does_nothing_when_no_indent
130 103
    with_settings :text_formatting => 'common_mark' do
131 104
      visit '/projects/ecookbook/issues/new'
132 105

  
133 106
      within('form#issue-form') do
134
        text = "1. item1\n1. item2\n1. item3"
135
        start = text.index("1. item2") + 5
136
        finish = start + 1
107
        text = "hello"
137 108
        el = find('#issue_description')
138 109
        el.click
139
        set_textarea_value 'issue_description', text, selection: [start, finish]
140
        el.send_keys(:tab)
141
        assert_equal "1. item1\n    1. item2\n1. item3", el.value
110
        set_textarea_value 'issue_description', text, selection: [0, text.length]
111
        el.send_keys([:shift, :tab])
112
        assert_equal "hello", el.value
142 113
      end
143 114
    end
144 115
  end
(3-3/3)