Skip to content

Commit b16a847

Browse files
authored
Merge pull request #53 from paxos/feature/cleanup-lists
Make list behaviors more consistent, fix list stacking
2 parents d36259a + 242cb0d commit b16a847

File tree

2 files changed

+318
-52
lines changed

2 files changed

+318
-52
lines changed

Diff for: src/index.ts

+126-46
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type Style = {
7979
replaceNext?: string
8080
scanFor?: string
8181
orderedList?: boolean
82+
unorderedList?: boolean
8283
prefixSpace?: boolean
8384
}
8485

@@ -205,7 +206,7 @@ if (!window.customElements.get('md-image')) {
205206
class MarkdownUnorderedListButtonElement extends MarkdownButtonElement {
206207
constructor() {
207208
super()
208-
styles.set(this, {prefix: '- ', multiline: true, surroundWithNewlines: true})
209+
styles.set(this, {prefix: '- ', multiline: true, unorderedList: true})
209210
}
210211
}
211212

@@ -421,8 +422,8 @@ function styleSelectedText(textarea: HTMLTextAreaElement, styleArgs: StyleArgs)
421422
const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
422423

423424
let result
424-
if (styleArgs.orderedList) {
425-
result = orderedList(textarea)
425+
if (styleArgs.orderedList || styleArgs.unorderedList) {
426+
result = listStyle(textarea, styleArgs)
426427
} else if (styleArgs.multiline && isMultipleLines(text)) {
427428
result = multilineStyle(textarea, styleArgs)
428429
} else {
@@ -432,6 +433,21 @@ function styleSelectedText(textarea: HTMLTextAreaElement, styleArgs: StyleArgs)
432433
insertText(textarea, result)
433434
}
434435

436+
function expandSelectionToLine(textarea: HTMLTextAreaElement) {
437+
const lines = textarea.value.split('\n')
438+
let counter = 0
439+
for (let index = 0; index < lines.length; index++) {
440+
const lineLength = lines[index].length + 1
441+
if (textarea.selectionStart >= counter && textarea.selectionStart < counter + lineLength) {
442+
textarea.selectionStart = counter
443+
}
444+
if (textarea.selectionEnd >= counter && textarea.selectionEnd < counter + lineLength) {
445+
textarea.selectionEnd = counter + lineLength - 1
446+
}
447+
counter += lineLength
448+
}
449+
}
450+
435451
function expandSelectedText(
436452
textarea: HTMLTextAreaElement,
437453
prefixToUse: string,
@@ -587,41 +603,115 @@ function multilineStyle(textarea: HTMLTextAreaElement, arg: StyleArgs) {
587603
return {text, selectionStart, selectionEnd}
588604
}
589605

590-
function orderedList(textarea: HTMLTextAreaElement): SelectionRange {
606+
interface UndoResult {
607+
text: string
608+
processed: boolean
609+
}
610+
function undoOrderedListStyle(text: string): UndoResult {
611+
const lines = text.split('\n')
591612
const orderedListRegex = /^\d+\.\s+/
613+
const shouldUndoOrderedList = lines.every(line => orderedListRegex.test(line))
614+
let result = lines
615+
if (shouldUndoOrderedList) {
616+
result = lines.map(line => line.replace(orderedListRegex, ''))
617+
}
618+
619+
return {
620+
text: result.join('\n'),
621+
processed: shouldUndoOrderedList
622+
}
623+
}
624+
625+
function undoUnorderedListStyle(text: string): UndoResult {
626+
const lines = text.split('\n')
627+
const unorderedListPrefix = '- '
628+
const shouldUndoUnorderedList = lines.every(line => line.startsWith(unorderedListPrefix))
629+
let result = lines
630+
if (shouldUndoUnorderedList) {
631+
result = lines.map(line => line.slice(unorderedListPrefix.length, line.length))
632+
}
633+
634+
return {
635+
text: result.join('\n'),
636+
processed: shouldUndoUnorderedList
637+
}
638+
}
639+
640+
function makePrefix(index: number, unorderedList: boolean): string {
641+
if (unorderedList) {
642+
return '- '
643+
} else {
644+
return `${index + 1}. `
645+
}
646+
}
647+
648+
function clearExistingListStyle(style: StyleArgs, selectedText: string): [UndoResult, UndoResult, string] {
649+
let undoResultOpositeList: UndoResult
650+
let undoResult: UndoResult
651+
let pristineText
652+
if (style.orderedList) {
653+
undoResult = undoOrderedListStyle(selectedText)
654+
undoResultOpositeList = undoUnorderedListStyle(undoResult.text)
655+
pristineText = undoResultOpositeList.text
656+
} else {
657+
undoResult = undoUnorderedListStyle(selectedText)
658+
undoResultOpositeList = undoOrderedListStyle(undoResult.text)
659+
pristineText = undoResultOpositeList.text
660+
}
661+
return [undoResult, undoResultOpositeList, pristineText]
662+
}
663+
664+
function listStyle(textarea: HTMLTextAreaElement, style: StyleArgs): SelectionRange {
592665
const noInitialSelection = textarea.selectionStart === textarea.selectionEnd
593-
let selectionEnd
594-
let selectionStart
595-
let text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
596-
let textToUnstyle = text
597-
let lines = text.split('\n')
598-
let startOfLine, endOfLine
599-
if (noInitialSelection) {
600-
const linesBefore = textarea.value.slice(0, textarea.selectionStart).split(/\n/)
601-
startOfLine = textarea.selectionStart - linesBefore[linesBefore.length - 1].length
602-
endOfLine = wordSelectionEnd(textarea.value, textarea.selectionStart, true)
603-
textToUnstyle = textarea.value.slice(startOfLine, endOfLine)
604-
}
605-
const linesToUnstyle = textToUnstyle.split('\n')
606-
const undoStyling = linesToUnstyle.every(line => orderedListRegex.test(line))
607-
608-
if (undoStyling) {
609-
lines = linesToUnstyle.map(line => line.replace(orderedListRegex, ''))
610-
text = lines.join('\n')
611-
if (noInitialSelection && startOfLine && endOfLine) {
612-
const lengthDiff = linesToUnstyle[0].length - lines[0].length
613-
selectionStart = selectionEnd = textarea.selectionStart - lengthDiff
614-
textarea.selectionStart = startOfLine
615-
textarea.selectionEnd = endOfLine
666+
let selectionStart = textarea.selectionStart
667+
let selectionEnd = textarea.selectionEnd
668+
669+
// Select whole line
670+
expandSelectionToLine(textarea)
671+
672+
const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
673+
674+
// If the user intent was to do an undo, we will stop after this.
675+
// Otherwise, we will still undo to other list type to prevent list stacking
676+
const [undoResult, undoResultOpositeList, pristineText] = clearExistingListStyle(style, selectedText)
677+
678+
const prefixedLines = pristineText.split('\n').map((value, index) => {
679+
return `${makePrefix(index, style.unorderedList)}${value}`
680+
})
681+
682+
const totalPrefixLength = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {
683+
return previousValue + makePrefix(currentIndex, style.unorderedList).length
684+
}, 0)
685+
686+
const totalPrefixLengthOpositeList = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => {
687+
return previousValue + makePrefix(currentIndex, !style.unorderedList).length
688+
}, 0)
689+
690+
if (undoResult.processed) {
691+
if (noInitialSelection) {
692+
selectionStart = Math.max(selectionStart - makePrefix(0, style.unorderedList).length, 0)
693+
selectionEnd = selectionStart
694+
} else {
695+
selectionStart = textarea.selectionStart
696+
selectionEnd = textarea.selectionEnd - totalPrefixLength
616697
}
698+
return {text: pristineText, selectionStart, selectionEnd}
699+
}
700+
701+
const {newlinesToAppend, newlinesToPrepend} = newlinesToSurroundSelectedText(textarea)
702+
const text = newlinesToAppend + prefixedLines.join('\n') + newlinesToPrepend
703+
704+
if (noInitialSelection) {
705+
selectionStart = Math.max(selectionStart + makePrefix(0, style.unorderedList).length + newlinesToAppend.length, 0)
706+
selectionEnd = selectionStart
617707
} else {
618-
lines = numberedLines(lines)
619-
text = lines.join('\n')
620-
const {newlinesToAppend, newlinesToPrepend} = newlinesToSurroundSelectedText(textarea)
621-
selectionStart = textarea.selectionStart + newlinesToAppend.length
622-
selectionEnd = selectionStart + text.length
623-
if (noInitialSelection) selectionStart = selectionEnd
624-
text = newlinesToAppend + text + newlinesToPrepend
708+
if (undoResultOpositeList.processed) {
709+
selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0)
710+
selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength - totalPrefixLengthOpositeList
711+
} else {
712+
selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0)
713+
selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength
714+
}
625715
}
626716

627717
return {text, selectionStart, selectionEnd}
@@ -638,21 +728,10 @@ interface StyleArgs {
638728
scanFor: string
639729
surroundWithNewlines: boolean
640730
orderedList: boolean
731+
unorderedList: boolean
641732
trimFirst: boolean
642733
}
643734

644-
function numberedLines(lines: string[]) {
645-
let i
646-
let len
647-
let index
648-
const results = []
649-
for (index = i = 0, len = lines.length; i < len; index = ++i) {
650-
const line = lines[index]
651-
results.push(`${index + 1}. ${line}`)
652-
}
653-
return results
654-
}
655-
656735
function applyStyle(button: Element, stylesToApply: Style) {
657736
const toolbar = button.closest('markdown-toolbar')
658737
if (!(toolbar instanceof MarkdownToolbarElement)) return
@@ -668,6 +747,7 @@ function applyStyle(button: Element, stylesToApply: Style) {
668747
scanFor: '',
669748
surroundWithNewlines: false,
670749
orderedList: false,
750+
unorderedList: false,
671751
trimFirst: false
672752
}
673753

0 commit comments

Comments
 (0)