mirror of
https://github.com/redmine/redmine.git
synced 2026-05-07 15:26:25 +02:00
Support automatic list marker insertion in textareas (#43095).
Patch by Mizuki ISHIKAWA (user:ishikawa999). git-svn-id: https://svn.redmine.org/redmine/trunk@24003 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
124
app/javascript/controllers/list_autofill_controller.js
Normal file
124
app/javascript/controllers/list_autofill_controller.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
class ListAutofillHandler {
|
||||
constructor(inputElement, format) {
|
||||
this.input = inputElement
|
||||
this.format = format
|
||||
}
|
||||
|
||||
run(event) {
|
||||
const { selectionStart, value } = this.input
|
||||
|
||||
const beforeCursor = value.slice(0, selectionStart)
|
||||
const lines = beforeCursor.split("\n")
|
||||
const currentLine = lines[lines.length - 1]
|
||||
const lineStartPos = beforeCursor.lastIndexOf("\n") + 1
|
||||
|
||||
let formatter
|
||||
switch (this.format) {
|
||||
case 'common_mark':
|
||||
formatter = new CommonMarkListFormatter()
|
||||
break
|
||||
case 'textile':
|
||||
formatter = new TextileListFormatter()
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const result = formatter.format(currentLine)
|
||||
|
||||
if (!result) return
|
||||
|
||||
switch (result.action) {
|
||||
case 'remove':
|
||||
event.preventDefault()
|
||||
this.input.setRangeText('', lineStartPos, selectionStart, 'start')
|
||||
break
|
||||
case 'insert':
|
||||
event.preventDefault()
|
||||
const insertText = "\n" + result.text
|
||||
const newValue = value.slice(0, selectionStart) + insertText + value.slice(selectionStart)
|
||||
const newCursor = selectionStart + insertText.length
|
||||
this.input.value = newValue
|
||||
this.input.setSelectionRange(newCursor, newCursor)
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CommonMarkListFormatter {
|
||||
format(line) {
|
||||
// Match list items in CommonMark syntax.
|
||||
// Captures either an ordered list (e.g., '1. ' or '2) ') or an unordered list (e.g., '* ', '- ', '+ ').
|
||||
// The regex structure:
|
||||
// ^(\s*) → leading whitespace
|
||||
// (?:(\d+)([.)]) → an ordered list marker: number followed by '.' or ')'
|
||||
// |([*+\-]) → OR an unordered list marker: '*', '+', or '-'
|
||||
// (.*)$ → the actual list item content
|
||||
//
|
||||
// Examples:
|
||||
// '2. ordered text' → indent='', number='2', delimiter='.', bullet=undefined, content='ordered text'
|
||||
// ' 3) nested ordered text' → indent=' ', number='3', delimiter=')', bullet=undefined, content='nested ordered text'
|
||||
// '* unordered text' → indent='', number=undefined, delimiter=undefined, bullet='*', content='unordered text'
|
||||
// '+ unordered text' → indent='', number=undefined, delimiter=undefined, bullet='+', content='unordered text'
|
||||
// ' - nested unordered text' → indent=' ', number=undefined, delimiter=undefined, bullet='-', content='nested unordered text'
|
||||
const match = line.match(/^(\s*)(?:(\d+)([.)])|([*+\-])) (.*)$/)
|
||||
if (!match) return null
|
||||
|
||||
const indent = match[1]
|
||||
const number = match[2]
|
||||
const delimiter = match[3]
|
||||
const bullet = match[4]
|
||||
const content = match[5]
|
||||
|
||||
if (content === '') {
|
||||
return { action: 'remove' }
|
||||
}
|
||||
|
||||
if (number) {
|
||||
const nextNumber = parseInt(number, 10) + 1
|
||||
return { action: 'insert', text: `${indent}${nextNumber}${delimiter} ` }
|
||||
} else {
|
||||
return { action: 'insert', text: `${indent}${bullet} ` }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TextileListFormatter {
|
||||
format(line) {
|
||||
// Match list items in Textile syntax.
|
||||
// Captures either an ordered list (using '#') or an unordered list (using '*').
|
||||
// The regex structure:
|
||||
// ^([*#]+) → one or more list markers: '*' for unordered, '#' for ordered
|
||||
// (.*)$ → the actual list item content
|
||||
//
|
||||
// Examples:
|
||||
// '# ordered text' → marker='#', content='ordered text'
|
||||
// '## nested ordered text' → marker='##', content='nested ordered text'
|
||||
// '* unordered text' → marker='*', content='unordered text'
|
||||
// '** nested unordered text' → marker='**', content='nested unordered text'
|
||||
const match = line.match(/^([*#]+) (.*)$/)
|
||||
if (!match) return null
|
||||
|
||||
const marker = match[1]
|
||||
const content = match[2]
|
||||
|
||||
if (content === '') {
|
||||
return { action: 'remove' }
|
||||
}
|
||||
|
||||
return { action: 'insert', text: `${marker} ` }
|
||||
}
|
||||
}
|
||||
|
||||
export default class extends Controller {
|
||||
handleBeforeInput(event) {
|
||||
if (event.inputType != 'insertLineBreak') return
|
||||
|
||||
const format = event.params.textFormatting
|
||||
new ListAutofillHandler(event.currentTarget, format).run(event)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user