Feature #44061 ยป 0001-Add-Tab-and-Shift-Tab-indentation-for-list-items-in-.patch
| app/helpers/application_helper.rb | ||
|---|---|---|
| 1433 | 1433 |
return {} if Setting.text_formatting.blank?
|
| 1434 | 1434 | |
| 1435 | 1435 |
{
|
| 1436 |
controller: 'list-autofill table-paste', |
|
| 1437 |
action: 'beforeinput->list-autofill#handleBeforeInput paste->table-paste#handlePaste', |
|
| 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',
|
|
| 1438 | 1438 |
list_autofill_text_formatting_param: Setting.text_formatting, |
| 1439 |
list_indent_text_formatting_param: Setting.text_formatting, |
|
| 1439 | 1440 |
table_paste_text_formatting_param: Setting.text_formatting |
| 1440 | 1441 |
} |
| 1441 | 1442 |
end |
| app/javascript/controllers/list_indent_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from '@hotwired/stimulus'
|
|
| 2 | ||
| 3 |
export default class extends Controller {
|
|
| 4 |
#bulletPattern = /^\s*[*+\-] / |
|
| 5 |
#orderedPattern = /^\s*\d+[.)] / |
|
| 6 | ||
| 7 |
run(event) {
|
|
| 8 |
const format = event.params.textFormatting |
|
| 9 |
if (format !== 'common_mark') return |
|
| 10 | ||
| 11 |
const input = event.currentTarget |
|
| 12 |
const { selectionStart, value } = input
|
|
| 13 |
const lineStart = value.lastIndexOf("\n", selectionStart - 1) + 1
|
|
| 14 |
const lineEnd = value.indexOf("\n", selectionStart)
|
|
| 15 |
const currentLine = value.slice(lineStart, lineEnd === -1 ? value.length : lineEnd) |
|
| 16 |
const spaces = this.#indentSize(currentLine) |
|
| 17 |
if (!spaces) return |
|
| 18 | ||
| 19 |
event.preventDefault() |
|
| 20 |
event.shiftKey |
|
| 21 |
? this.#unindent(input, lineStart, currentLine, selectionStart, spaces) |
|
| 22 |
: this.#indent(input, lineStart, selectionStart, spaces) |
|
| 23 |
} |
|
| 24 | ||
| 25 |
#indentSize(line) {
|
|
| 26 |
if (this.#bulletPattern.test(line)) return 2 |
|
| 27 |
if (this.#orderedPattern.test(line)) return 4 |
|
| 28 |
return 0 |
|
| 29 |
} |
|
| 30 | ||
| 31 |
#indent(input, lineStart, cursorPos, spaces) {
|
|
| 32 |
input.setRangeText(' '.repeat(spaces), lineStart, lineStart, 'preserve')
|
|
| 33 |
input.setSelectionRange(cursorPos + spaces, cursorPos + spaces) |
|
| 34 |
} |
|
| 35 | ||
| 36 |
#unindent(input, lineStart, currentLine, cursorPos, spaces) {
|
|
| 37 |
const currentIndent = currentLine.match(/^(\s*)/)[1].length |
|
| 38 |
const remove = Math.min(spaces, currentIndent) |
|
| 39 |
if (remove === 0) return |
|
| 40 |
input.setRangeText('', lineStart, lineStart + remove, 'preserve')
|
|
| 41 |
const newCursor = Math.max(lineStart, cursorPos - remove) |
|
| 42 |
input.setSelectionRange(newCursor, newCursor) |
|
| 43 |
} |
|
| 44 |
} |
|
| test/system/list_indent_test.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
require_relative '../application_system_test_case' |
|
| 4 | ||
| 5 |
class ListIndentSystemTest < ApplicationSystemTestCase |
|
| 6 |
def setup |
|
| 7 |
super |
|
| 8 |
log_user('jsmith', 'jsmith')
|
|
| 9 |
end |
|
| 10 | ||
| 11 |
# Tab: inserts 2 spaces for bullet lists, 4 spaces for ordered lists |
|
| 12 |
def test_tab_indents_common_mark_bullet_list |
|
| 13 |
with_settings :text_formatting => 'common_mark' do |
|
| 14 |
visit '/projects/ecookbook/issues/new' |
|
| 15 | ||
| 16 |
within('form#issue-form') do
|
|
| 17 |
fill_in 'Description', with: "- item" |
|
| 18 |
find('#issue_description').send_keys(:tab)
|
|
| 19 | ||
| 20 |
assert_equal " - item", find('#issue_description').value
|
|
| 21 |
end |
|
| 22 |
end |
|
| 23 |
end |
|
| 24 | ||
| 25 |
def test_tab_indents_common_mark_ordered_list |
|
| 26 |
with_settings :text_formatting => 'common_mark' do |
|
| 27 |
visit '/projects/ecookbook/issues/new' |
|
| 28 | ||
| 29 |
within('form#issue-form') do
|
|
| 30 |
fill_in 'Description', with: "1. item" |
|
| 31 |
find('#issue_description').send_keys(:tab)
|
|
| 32 | ||
| 33 |
assert_equal " 1. item", find('#issue_description').value
|
|
| 34 |
end |
|
| 35 |
end |
|
| 36 |
end |
|
| 37 | ||
| 38 |
# Shift+Tab: removes indentation |
|
| 39 |
def test_shift_tab_unindents_common_mark_bullet_list |
|
| 40 |
with_settings :text_formatting => 'common_mark' do |
|
| 41 |
visit '/projects/ecookbook/issues/new' |
|
| 42 | ||
| 43 |
within('form#issue-form') do
|
|
| 44 |
set_textarea_value 'issue_description', "- parent\n - child" |
|
| 45 |
find('#issue_description').send_keys([:shift, :tab])
|
|
| 46 | ||
| 47 |
assert_equal "- parent\n- child", find('#issue_description').value
|
|
| 48 |
end |
|
| 49 |
end |
|
| 50 |
end |
|
| 51 | ||
| 52 |
def test_shift_tab_removes_partial_indent_on_common_mark_list |
|
| 53 |
# Removes only as many spaces as exist when indent is less than the step size |
|
| 54 |
with_settings :text_formatting => 'common_mark' do |
|
| 55 |
visit '/projects/ecookbook/issues/new' |
|
| 56 | ||
| 57 |
within('form#issue-form') do
|
|
| 58 |
set_textarea_value 'issue_description', "- parent\n - child" |
|
| 59 |
find('#issue_description').send_keys([:shift, :tab])
|
|
| 60 | ||
| 61 |
assert_equal "- parent\n- child", find('#issue_description').value
|
|
| 62 |
end |
|
| 63 |
end |
|
| 64 |
end |
|
| 65 | ||
| 66 |
def test_shift_tab_does_nothing_on_common_mark_list_with_no_indent |
|
| 67 |
with_settings :text_formatting => 'common_mark' do |
|
| 68 |
visit '/projects/ecookbook/issues/new' |
|
| 69 | ||
| 70 |
within('form#issue-form') do
|
|
| 71 |
fill_in 'Description', with: "- item" |
|
| 72 |
find('#issue_description').send_keys([:shift, :tab])
|
|
| 73 | ||
| 74 |
assert_equal "- item", find('#issue_description').value
|
|
| 75 |
end |
|
| 76 |
end |
|
| 77 |
end |
|
| 78 | ||
| 79 |
private |
|
| 80 | ||
| 81 |
# Use execute_script to set multi-line values directly via JS, |
|
| 82 |
# since fill_in sends keystrokes which trigger the list autofill handler. |
|
| 83 |
def set_textarea_value(id, text) |
|
| 84 |
page.execute_script( |
|
| 85 |
"const el = document.getElementById(arguments[0]);" \ |
|
| 86 |
"el.value = arguments[1];" \ |
|
| 87 |
"el.setSelectionRange(el.value.length, el.value.length);", |
|
| 88 |
id, |
|
| 89 |
text |
|
| 90 |
) |
|
| 91 |
end |
|
| 92 |
end |
|