diff --git a/examples/index.html b/examples/index.html index 63c730e..f1f153e 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,61 +1,72 @@ - + - - - paste-markdown demo - - - -

Test by selecting the elements and then either copy/paste or drag them into the textarea.

- - - - - - - - - - - - - - - - - -
nameorigin
hubotgithub
benderfuturama
+ + + paste-markdown demo + + + +

Test by selecting the elements and then either copy/paste or drag them into the textarea.

+ + + + + + + + + + + + + + + + + +
nameorigin
hubotgithub
benderfuturama
- - - - - - - - - - - - - -
nameorigin
this table will not beconverted to markdown
+ + + + + + + + + + + + + +
nameorigin
this table will not beconverted to markdown
- hubot + hubot -

Test by copying this page's URL and then selecting here in the textarea and pasting the URL.

+

Test by copying this page's URL and then selecting here in the textarea and pasting the URL.

-

Or copy and paste a link and another link and maybe one more into the textarea.

+

+ Or copy and paste a link and + another link and maybe + one more into the textarea. +

- + - - + + diff --git a/src/index.ts b/src/index.ts index c3c37fa..2f84aee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,10 @@ import {install as installHTML, uninstall as uninstallHTML} from './paste-markdown-html' import {install as installImageLink, uninstall as uninstallImageLink} from './paste-markdown-image-link' import {install as installLink, uninstall as uninstallLink} from './paste-markdown-link' +import { + installAround as installSkipFormatting, + uninstall as uninstallSkipFormatting +} from './paste-keyboard-shortcut-helper' import {install as installTable, uninstall as uninstallTable} from './paste-markdown-table' import {install as installText, uninstall as uninstallText} from './paste-markdown-text' @@ -9,14 +13,11 @@ interface Subscription { } function subscribe(el: HTMLElement): Subscription { - installTable(el) - installImageLink(el) - installLink(el) - installText(el) - installHTML(el) + installSkipFormatting(el, installTable, installImageLink, installLink, installText, installHTML) return { unsubscribe: () => { + uninstallSkipFormatting(el) uninstallTable(el) uninstallHTML(el) uninstallImageLink(el) diff --git a/src/paste-keyboard-shortcut-helper.ts b/src/paste-keyboard-shortcut-helper.ts new file mode 100644 index 0000000..f2a5261 --- /dev/null +++ b/src/paste-keyboard-shortcut-helper.ts @@ -0,0 +1,37 @@ +const skipFormattingMap = new WeakMap() + +function setSkipFormattingFlag(event: KeyboardEvent): void { + const {currentTarget: el} = event + const isSkipFormattingKeys = event.code === 'KeyV' && (event.ctrlKey || event.metaKey) && event.shiftKey + + // Supports Cmd+Shift+V (Chrome) / Cmd+Shift+Opt+V (Safari, Firefox and Edge) to mimic paste and match style shortcut on MacOS. + if (isSkipFormattingKeys || (isSkipFormattingKeys && event.altKey)) { + skipFormattingMap.set(el as HTMLElement, true) + } +} + +function unsetSkipFormattedFlag(event: ClipboardEvent): void { + const {currentTarget: el} = event + skipFormattingMap.delete(el as HTMLElement) +} + +export function shouldSkipFormatting(el: HTMLElement): boolean { + const shouldSkipFormattingState = skipFormattingMap.get(el) ?? false + + return shouldSkipFormattingState +} + +export function installAround(el: HTMLElement, ...installCallbacks: Array<(el: HTMLElement) => void>): void { + el.addEventListener('keydown', setSkipFormattingFlag) + + for (const installCallback of installCallbacks) { + installCallback(el) + } + + el.addEventListener('paste', unsetSkipFormattedFlag) +} + +export function uninstall(el: HTMLElement): void { + el.removeEventListener('keydown', setSkipFormattingFlag) + el.removeEventListener('paste', unsetSkipFormattedFlag) +} diff --git a/src/paste-markdown-html.ts b/src/paste-markdown-html.ts index 3283f84..8b1ae17 100644 --- a/src/paste-markdown-html.ts +++ b/src/paste-markdown-html.ts @@ -1,4 +1,5 @@ import {insertText} from './text' +import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper' export function install(el: HTMLElement): void { el.addEventListener('paste', onPaste) @@ -10,6 +11,8 @@ export function uninstall(el: HTMLElement): void { function onPaste(event: ClipboardEvent) { const transfer = event.clipboardData + const {currentTarget: el} = event + if (shouldSkipFormatting(el as HTMLElement)) return // if there is no clipboard data, or // if there is no html content in the clipboard, return if (!transfer || !hasHTML(transfer)) return diff --git a/src/paste-markdown-image-link.ts b/src/paste-markdown-image-link.ts index defdcb2..b3db644 100644 --- a/src/paste-markdown-image-link.ts +++ b/src/paste-markdown-image-link.ts @@ -1,6 +1,6 @@ /* @flow strict */ - import {insertText} from './text' +import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper' export function install(el: HTMLElement): void { el.addEventListener('dragover', onDragover) @@ -38,6 +38,9 @@ function onDragover(event: DragEvent) { } function onPaste(event: ClipboardEvent) { + const {currentTarget: el} = event + if (shouldSkipFormatting(el as HTMLElement)) return + const transfer = event.clipboardData if (!transfer || !hasLink(transfer)) return diff --git a/src/paste-markdown-link.ts b/src/paste-markdown-link.ts index 1bbf17c..1c9674d 100644 --- a/src/paste-markdown-link.ts +++ b/src/paste-markdown-link.ts @@ -1,4 +1,5 @@ import {insertText} from './text' +import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper' export function install(el: HTMLElement): void { el.addEventListener('paste', onPaste) @@ -9,6 +10,9 @@ export function uninstall(el: HTMLElement): void { } function onPaste(event: ClipboardEvent) { + const {currentTarget: el} = event + if (shouldSkipFormatting(el as HTMLElement)) return + const transfer = event.clipboardData if (!transfer || !hasPlainText(transfer)) return diff --git a/src/paste-markdown-table.ts b/src/paste-markdown-table.ts index e2784b7..afa5294 100644 --- a/src/paste-markdown-table.ts +++ b/src/paste-markdown-table.ts @@ -1,4 +1,5 @@ import {insertText} from './text' +import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper' export function install(el: HTMLElement): void { el.addEventListener('dragover', onDragover) @@ -35,6 +36,9 @@ function onDragover(event: DragEvent) { } function onPaste(event: ClipboardEvent) { + const {currentTarget: el} = event + if (shouldSkipFormatting(el as HTMLElement)) return + if (!event.clipboardData) return const textToPaste = generateText(event.clipboardData) diff --git a/src/paste-markdown-text.ts b/src/paste-markdown-text.ts index b02fa6d..f041402 100644 --- a/src/paste-markdown-text.ts +++ b/src/paste-markdown-text.ts @@ -1,4 +1,5 @@ import {insertText} from './text' +import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper' export function install(el: HTMLElement): void { el.addEventListener('paste', onPaste) @@ -9,6 +10,9 @@ export function uninstall(el: HTMLElement): void { } function onPaste(event: ClipboardEvent) { + const {currentTarget: el} = event + if (shouldSkipFormatting(el as HTMLElement)) return + const transfer = event.clipboardData if (!transfer || !hasMarkdown(transfer)) return diff --git a/test/test.js b/test/test.js index 526f88a..8020f6e 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,17 @@ import {subscribe} from '../dist/index.esm.js' +const tableHtml = ` + + + + + + +
nameorigin
hubotgithub
benderfuturama
+` + +const tableMarkdown = 'name | origin\n-- | --\nhubot | github\nbender | futurama' + describe('paste-markdown', function () { describe('installed on textarea', function () { let subscription @@ -43,18 +55,10 @@ describe('paste-markdown', function () { it('turns html tables into markdown', function () { const data = { - 'text/html': ` - - - - - - -
nameorigin
hubotgithub
benderfuturama
- ` + 'text/html': tableHtml } paste(textarea, data) - assert.include(textarea.value, 'name | origin\n-- | --\nhubot | github\nbender | futurama') + assert.include(textarea.value, tableMarkdown) }) it("doesn't execute JavaScript", async function () { @@ -76,13 +80,7 @@ describe('paste-markdown', function () { const data = { 'text/html': `

Here is a cool table

- - - - - - -
nameorigin
hubotgithub
benderfuturama
+ ${tableHtml}

Very cool

` } @@ -91,7 +89,7 @@ describe('paste-markdown', function () { assert.equal( textarea.value.trim(), // eslint-disable-next-line github/unescaped-html-literal - '

Here is a cool table

\n \n\nname | origin\n-- | --\nhubot | github\nbender | futurama\n\n\n

Very cool

' + `

Here is a cool table

\n \n \n\n${tableMarkdown}\n\n\n\n

Very cool

` ) }) @@ -111,7 +109,7 @@ describe('paste-markdown', function () { // Synthetic paste events don't manipulate the DOM. A empty textarea // means that the event handler didn't fire and normal paste happened. - assert.equal(textarea.value, '') + assertUnformattedPaste(textarea) }) it('accepts x-gfm', function () { @@ -213,9 +211,45 @@ describe('paste-markdown', function () { paste(textarea, {'text/html': sentence, 'text/plain': plaintextSentence}) assert.equal(textarea.value, markdownSentence) }) + + it('skip markdown formatting with (Ctrl+Shift+v)', function () { + const data = { + 'text/html': tableHtml + } + + dispatchSkipFormattingKeyEvent(textarea) + paste(textarea, data) + assertUnformattedPaste(textarea) + + textarea.value = '' + paste(textarea, data) + assert.include(textarea.value, tableMarkdown) + }) }) }) +/** + * Note: It's possible to construct and dispatch a synthetic paste event, + * but this will not affect the document's contents in tests to assert it. + * https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event + * So for that reason assert result on keydown (Ctrl+Shift+v) will be empty '' here. + */ +function assertUnformattedPaste(textarea) { + return assert.equal(textarea.value, '') +} + +function dispatchSkipFormattingKeyEvent(textarea) { + textarea.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'v', + code: 'KeyV', + shiftKey: true, + ctrlKey: true, + metaKey: true + }) + ) +} + function paste(textarea, data) { const dataTransfer = new DataTransfer() for (const key in data) {