diff --git a/src/index.ts b/src/index.ts index 87162e7..feb47c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,6 +79,7 @@ type Style = { replaceNext?: string scanFor?: string orderedList?: boolean + unorderedList?: boolean prefixSpace?: boolean } @@ -205,7 +206,7 @@ if (!window.customElements.get('md-image')) { class MarkdownUnorderedListButtonElement extends MarkdownButtonElement { constructor() { super() - styles.set(this, {prefix: '- ', multiline: true, surroundWithNewlines: true}) + styles.set(this, {prefix: '- ', multiline: true, unorderedList: true}) } } @@ -421,8 +422,8 @@ function styleSelectedText(textarea: HTMLTextAreaElement, styleArgs: StyleArgs) const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd) let result - if (styleArgs.orderedList) { - result = orderedList(textarea) + if (styleArgs.orderedList || styleArgs.unorderedList) { + result = listStyle(textarea, styleArgs) } else if (styleArgs.multiline && isMultipleLines(text)) { result = multilineStyle(textarea, styleArgs) } else { @@ -432,6 +433,21 @@ function styleSelectedText(textarea: HTMLTextAreaElement, styleArgs: StyleArgs) insertText(textarea, result) } +function expandSelectionToLine(textarea: HTMLTextAreaElement) { + const lines = textarea.value.split('\n') + let counter = 0 + for (let index = 0; index < lines.length; index++) { + const lineLength = lines[index].length + 1 + if (textarea.selectionStart >= counter && textarea.selectionStart < counter + lineLength) { + textarea.selectionStart = counter + } + if (textarea.selectionEnd >= counter && textarea.selectionEnd < counter + lineLength) { + textarea.selectionEnd = counter + lineLength - 1 + } + counter += lineLength + } +} + function expandSelectedText( textarea: HTMLTextAreaElement, prefixToUse: string, @@ -587,41 +603,115 @@ function multilineStyle(textarea: HTMLTextAreaElement, arg: StyleArgs) { return {text, selectionStart, selectionEnd} } -function orderedList(textarea: HTMLTextAreaElement): SelectionRange { +interface UndoResult { + text: string + processed: boolean +} +function undoOrderedListStyle(text: string): UndoResult { + const lines = text.split('\n') const orderedListRegex = /^\d+\.\s+/ + const shouldUndoOrderedList = lines.every(line => orderedListRegex.test(line)) + let result = lines + if (shouldUndoOrderedList) { + result = lines.map(line => line.replace(orderedListRegex, '')) + } + + return { + text: result.join('\n'), + processed: shouldUndoOrderedList + } +} + +function undoUnorderedListStyle(text: string): UndoResult { + const lines = text.split('\n') + const unorderedListPrefix = '- ' + const shouldUndoUnorderedList = lines.every(line => line.startsWith(unorderedListPrefix)) + let result = lines + if (shouldUndoUnorderedList) { + result = lines.map(line => line.slice(unorderedListPrefix.length, line.length)) + } + + return { + text: result.join('\n'), + processed: shouldUndoUnorderedList + } +} + +function makePrefix(index: number, unorderedList: boolean): string { + if (unorderedList) { + return '- ' + } else { + return `${index + 1}. ` + } +} + +function clearExistingListStyle(style: StyleArgs, selectedText: string): [UndoResult, UndoResult, string] { + let undoResultOpositeList: UndoResult + let undoResult: UndoResult + let pristineText + if (style.orderedList) { + undoResult = undoOrderedListStyle(selectedText) + undoResultOpositeList = undoUnorderedListStyle(undoResult.text) + pristineText = undoResultOpositeList.text + } else { + undoResult = undoUnorderedListStyle(selectedText) + undoResultOpositeList = undoOrderedListStyle(undoResult.text) + pristineText = undoResultOpositeList.text + } + return [undoResult, undoResultOpositeList, pristineText] +} + +function listStyle(textarea: HTMLTextAreaElement, style: StyleArgs): SelectionRange { const noInitialSelection = textarea.selectionStart === textarea.selectionEnd - let selectionEnd - let selectionStart - let text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd) - let textToUnstyle = text - let lines = text.split('\n') - let startOfLine, endOfLine - if (noInitialSelection) { - const linesBefore = textarea.value.slice(0, textarea.selectionStart).split(/\n/) - startOfLine = textarea.selectionStart - linesBefore[linesBefore.length - 1].length - endOfLine = wordSelectionEnd(textarea.value, textarea.selectionStart, true) - textToUnstyle = textarea.value.slice(startOfLine, endOfLine) - } - const linesToUnstyle = textToUnstyle.split('\n') - const undoStyling = linesToUnstyle.every(line => orderedListRegex.test(line)) - - if (undoStyling) { - lines = linesToUnstyle.map(line => line.replace(orderedListRegex, '')) - text = lines.join('\n') - if (noInitialSelection && startOfLine && endOfLine) { - const lengthDiff = linesToUnstyle[0].length - lines[0].length - selectionStart = selectionEnd = textarea.selectionStart - lengthDiff - textarea.selectionStart = startOfLine - textarea.selectionEnd = endOfLine + let selectionStart = textarea.selectionStart + let selectionEnd = textarea.selectionEnd + + // Select whole line + expandSelectionToLine(textarea) + + const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd) + + // If the user intent was to do an undo, we will stop after this. + // Otherwise, we will still undo to other list type to prevent list stacking + const [undoResult, undoResultOpositeList, pristineText] = clearExistingListStyle(style, selectedText) + + const prefixedLines = pristineText.split('\n').map((value, index) => { + return `${makePrefix(index, style.unorderedList)}${value}` + }) + + const totalPrefixLength = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => { + return previousValue + makePrefix(currentIndex, style.unorderedList).length + }, 0) + + const totalPrefixLengthOpositeList = prefixedLines.reduce((previousValue, _currentValue, currentIndex) => { + return previousValue + makePrefix(currentIndex, !style.unorderedList).length + }, 0) + + if (undoResult.processed) { + if (noInitialSelection) { + selectionStart = Math.max(selectionStart - makePrefix(0, style.unorderedList).length, 0) + selectionEnd = selectionStart + } else { + selectionStart = textarea.selectionStart + selectionEnd = textarea.selectionEnd - totalPrefixLength } + return {text: pristineText, selectionStart, selectionEnd} + } + + const {newlinesToAppend, newlinesToPrepend} = newlinesToSurroundSelectedText(textarea) + const text = newlinesToAppend + prefixedLines.join('\n') + newlinesToPrepend + + if (noInitialSelection) { + selectionStart = Math.max(selectionStart + makePrefix(0, style.unorderedList).length + newlinesToAppend.length, 0) + selectionEnd = selectionStart } else { - lines = numberedLines(lines) - text = lines.join('\n') - const {newlinesToAppend, newlinesToPrepend} = newlinesToSurroundSelectedText(textarea) - selectionStart = textarea.selectionStart + newlinesToAppend.length - selectionEnd = selectionStart + text.length - if (noInitialSelection) selectionStart = selectionEnd - text = newlinesToAppend + text + newlinesToPrepend + if (undoResultOpositeList.processed) { + selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0) + selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength - totalPrefixLengthOpositeList + } else { + selectionStart = Math.max(textarea.selectionStart + newlinesToAppend.length, 0) + selectionEnd = textarea.selectionEnd + newlinesToAppend.length + totalPrefixLength + } } return {text, selectionStart, selectionEnd} @@ -638,21 +728,10 @@ interface StyleArgs { scanFor: string surroundWithNewlines: boolean orderedList: boolean + unorderedList: boolean trimFirst: boolean } -function numberedLines(lines: string[]) { - let i - let len - let index - const results = [] - for (index = i = 0, len = lines.length; i < len; index = ++i) { - const line = lines[index] - results.push(`${index + 1}. ${line}`) - } - return results -} - function applyStyle(button: Element, stylesToApply: Style) { const toolbar = button.closest('markdown-toolbar') if (!(toolbar instanceof MarkdownToolbarElement)) return @@ -668,6 +747,7 @@ function applyStyle(button: Element, stylesToApply: Style) { scanFor: '', surroundWithNewlines: false, orderedList: false, + unorderedList: false, trimFirst: false } diff --git a/test/test.js b/test/test.js index ab8915e..c6279c1 100644 --- a/test/test.js +++ b/test/test.js @@ -484,11 +484,197 @@ describe('markdown-toolbar-element', function () { }) }) + describe('ordered list', function () { + it('turns line into list if cursor at end of line', function () { + setVisualValue('One\nTwo|\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\n1. Two|\n\nThree\n', visualValue()) + }) + + it('turns line into list if cursor at end of document', function () { + setVisualValue('One\nTwo\nThree|') + clickToolbar('md-ordered-list') + assert.equal('One\nTwo\n\n1. Three|', visualValue()) + }) + + it('turns line into list if cursor at beginning of line', function () { + setVisualValue('One\n|Two\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\n1. |Two\n\nThree\n', visualValue()) + }) + + it('turns line into list if cursor at middle of line', function () { + setVisualValue('One\nT|wo\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\n1. T|wo\n\nThree\n', visualValue()) + }) + + it('turns line into list if partial line is selected', function () { + setVisualValue('One\nT|w|o\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\n|1. Two|\n\nThree\n', visualValue()) + }) + + it('turns two lines into list if two lines are selected', function () { + setVisualValue('|One\nTwo|\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('|1. One\n2. Two|\n\nThree\n', visualValue()) + }) + + it('turns two lines into list if 2 lines are partially selected', function () { + setVisualValue('O|ne\nTw|o\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('|1. One\n2. Two|\n\nThree\n', visualValue()) + }) + + it('undo list if cursor at end of line', function () { + setVisualValue('One\n\n1. Two|\n\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\nTwo|\n\nThree\n', visualValue()) + }) + + it('undo list if cursor at end of document', function () { + setVisualValue('One\nTwo\n\n1. Three|') + clickToolbar('md-ordered-list') + assert.equal('One\nTwo\n\nThree|', visualValue()) + }) + + it('undo list if cursor at beginning of line', function () { + setVisualValue('One\n\n1. |Two\n\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\n|Two\n\nThree\n', visualValue()) + }) + + it('undo list if cursor at middle of line', function () { + setVisualValue('One\n\n1. T|wo\n\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\nT|wo\n\nThree\n', visualValue()) + }) + + it('undo list if partial line is selected', function () { + setVisualValue('One\n\n1. T|w|o\n\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('One\n\n|Two|\n\nThree\n', visualValue()) + }) + + it('undo two lines list if two lines are selected', function () { + setVisualValue('|1. One\n2. Two|\n\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('|One\nTwo|\n\nThree\n', visualValue()) + }) + + it('undo two lines list if 2 lines are partially selected', function () { + setVisualValue('1. O|ne\n2. Tw|o\n\nThree\n') + clickToolbar('md-ordered-list') + assert.equal('|One\nTwo|\n\nThree\n', visualValue()) + }) + }) + + describe('unordered list', function () { + it('turns line into list if cursor at end of line', function () { + setVisualValue('One\nTwo|\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\n- Two|\n\nThree\n', visualValue()) + }) + + it('turns line into list if cursor at end of document', function () { + setVisualValue('One\nTwo\nThree|') + clickToolbar('md-unordered-list') + assert.equal('One\nTwo\n\n- Three|', visualValue()) + }) + + it('turns line into list if cursor at beginning of line', function () { + setVisualValue('One\n|Two\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\n- |Two\n\nThree\n', visualValue()) + }) + + it('turns line into list if cursor at middle of line', function () { + setVisualValue('One\nT|wo\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\n- T|wo\n\nThree\n', visualValue()) + }) + + it('turns line into list if partial line is selected', function () { + setVisualValue('One\nT|w|o\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\n|- Two|\n\nThree\n', visualValue()) + }) + + it('turns two lines into list if two lines are selected', function () { + setVisualValue('|One\nTwo|\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('|- One\n- Two|\n\nThree\n', visualValue()) + }) + + it('turns two lines into list if 2 lines are partially selected', function () { + setVisualValue('O|ne\nTw|o\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('|- One\n- Two|\n\nThree\n', visualValue()) + }) + + it('undo list if cursor at end of line', function () { + setVisualValue('One\n\n- Two|\n\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\nTwo|\n\nThree\n', visualValue()) + }) + + it('undo list if cursor at end of document', function () { + setVisualValue('One\nTwo\n\n- Three|') + clickToolbar('md-unordered-list') + assert.equal('One\nTwo\n\nThree|', visualValue()) + }) + + it('undo list if cursor at beginning of line', function () { + setVisualValue('One\n\n- |Two\n\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\n|Two\n\nThree\n', visualValue()) + }) + + it('undo list if cursor at middle of line', function () { + setVisualValue('One\n\n- T|wo\n\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\nT|wo\n\nThree\n', visualValue()) + }) + + it('undo list if partial line is selected', function () { + setVisualValue('One\n\n- T|w|o\n\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('One\n\n|Two|\n\nThree\n', visualValue()) + }) + + it('undo two lines list if two lines are selected', function () { + setVisualValue('|- One\n- Two|\n\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('|One\nTwo|\n\nThree\n', visualValue()) + }) + + it('undo two lines list if 2 lines are partially selected', function () { + setVisualValue('- O|ne\n- Tw|o\n\nThree\n') + clickToolbar('md-unordered-list') + assert.equal('|One\nTwo|\n\nThree\n', visualValue()) + }) + }) + describe('lists', function () { + it('does not stack list styles when selecting multiple lines', function () { + setVisualValue('One\n|Two\nThree|\n') + clickToolbar('md-ordered-list') + clickToolbar('md-unordered-list') + assert.equal('One\n\n|- Two\n- Three|\n', visualValue()) + }) + + it('does not stack list styles when selecting one line', function () { + setVisualValue('One\n|Two|\nThree\n') + clickToolbar('md-ordered-list') + clickToolbar('md-unordered-list') + assert.equal('One\n\n|- Two|\n\nThree\n', visualValue()) + }) + it('turns line into list when you click the unordered list icon with selection', function () { setVisualValue('One\n|Two|\nThree\n') clickToolbar('md-unordered-list') - assert.equal('One\n\n- |Two|\n\nThree\n', visualValue()) + assert.equal('One\n\n|- Two|\n\nThree\n', visualValue()) }) it('turns line into list when you click the unordered list icon without selection', function () { @@ -504,21 +690,21 @@ describe('markdown-toolbar-element', function () { }) it('prefixes newlines when a list is created on the last line', function () { - setVisualValue("Here's a list:|One|") + setVisualValue("Here's a |list:|") clickToolbar('md-unordered-list') - assert.equal("Here's a list:\n\n- |One|", visualValue()) + assert.equal("|- Here's a list:|", visualValue()) }) it('surrounds list with newlines when a list is created on an existing line', function () { setVisualValue("Here's a list:|One|\nThis is text after the list") clickToolbar('md-unordered-list') - assert.equal("Here's a list:\n\n- |One|\n\nThis is text after the list", visualValue()) + assert.equal("|- Here's a list:One|\n\nThis is text after the list", visualValue()) }) it('undo the list when button is clicked again', function () { setVisualValue('|Two|') clickToolbar('md-unordered-list') - assert.equal('- |Two|', visualValue()) + assert.equal('|- Two|', visualValue()) clickToolbar('md-unordered-list') assert.equal('|Two|', visualValue()) }) @@ -526,7 +712,7 @@ describe('markdown-toolbar-element', function () { it('creates ordered list without selection', function () { setVisualValue('apple\n|pear\nbanana\n') clickToolbar('md-ordered-list') - assert.equal('apple\n\n1. |\n\npear\nbanana\n', visualValue()) + assert.equal('apple\n\n1. |pear\n\nbanana\n', visualValue()) }) it('undo an ordered list without selection', function () {