diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 7f3b8c8..c01748a 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,6 +1,11 @@ name: Node CI -on: push +on: + workflow_dispatch: + pull_request: + push: + branches: + - main jobs: build: runs-on: ubuntu-latest diff --git a/package-lock.json b/package-lock.json index d566780..0c925f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@github/markdown-toolbar-element", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 898188d..ccdeb30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@github/markdown-toolbar-element", - "version": "2.0.0", + "version": "2.1.0", "description": "Markdown formatting buttons for text inputs.", "repository": "github/markdown-toolbar-element", "type": "module", 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 () {
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: