Skip to content

Commit 8cdc3a6

Browse files
authored
Merge pull request #59 from zutshisunakshi/upstream/theinterned/unformatted-before-and-after
Support keyboard shortcut for formatted Vs Unformatted paste
2 parents 287767d + 5a35b5b commit 8cdc3a6

File tree

9 files changed

+179
-78
lines changed

9 files changed

+179
-78
lines changed

examples/index.html

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,72 @@
1-
<!doctype html>
1+
<!DOCTYPE html>
22
<html lang="en">
3-
<head>
4-
<meta charset="utf-8">
5-
<title>paste-markdown demo</title>
6-
<style>
7-
img, textarea { display: block; }
8-
img, table, textarea { margin: 1em 0; }
9-
</style>
10-
</head>
11-
<body>
12-
<p>Test by selecting the elements and then either copy/paste or drag them into the textarea.</p>
13-
<table border="1" cellpadding="5">
14-
<thead>
15-
<tr>
16-
<th>name</th>
17-
<th>origin</th>
18-
</tr>
19-
</thead>
20-
<tbody>
21-
<tr>
22-
<td>hubot</td>
23-
<td>github</td>
24-
</tr>
25-
<tr>
26-
<td>bender</td>
27-
<td>futurama</td>
28-
</tr>
29-
</tbody>
30-
</table>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>paste-markdown demo</title>
6+
<style>
7+
img,
8+
textarea {
9+
display: block;
10+
}
11+
img,
12+
table,
13+
textarea {
14+
margin: 1em 0;
15+
}
16+
</style>
17+
</head>
18+
<body>
19+
<p>Test by selecting the elements and then either copy/paste or drag them into the textarea.</p>
20+
<table border="1" cellpadding="5">
21+
<thead>
22+
<tr>
23+
<th>name</th>
24+
<th>origin</th>
25+
</tr>
26+
</thead>
27+
<tbody>
28+
<tr>
29+
<td>hubot</td>
30+
<td>github</td>
31+
</tr>
32+
<tr>
33+
<td>bender</td>
34+
<td>futurama</td>
35+
</tr>
36+
</tbody>
37+
</table>
3138

32-
<table border="1" cellpadding="5" data-paste-markdown-skip>
33-
<thead>
34-
<tr>
35-
<th>name</th>
36-
<th>origin</th>
37-
</tr>
38-
</thead>
39-
<tbody>
40-
<tr>
41-
<td>this table will not be</td>
42-
<td>converted to markdown</td>
43-
</tr>
44-
</tbody>
45-
</table>
39+
<table border="1" cellpadding="5" data-paste-markdown-skip>
40+
<thead>
41+
<tr>
42+
<th>name</th>
43+
<th>origin</th>
44+
</tr>
45+
</thead>
46+
<tbody>
47+
<tr>
48+
<td>this table will not be</td>
49+
<td>converted to markdown</td>
50+
</tr>
51+
</tbody>
52+
</table>
4653

47-
<img src="https://github.com/hubot.png" width="100" alt="hubot">
54+
<img src="https://github.com/hubot.png" width="100" alt="hubot" />
4855

49-
<p>Test by copying this page's URL and then selecting <i>here</i> in the textarea and pasting the URL.</p>
56+
<p>Test by copying this page's URL and then selecting <i>here</i> in the textarea and pasting the URL.</p>
5057

51-
<p>Or copy and paste a <a href="https://github.com">link</a> and <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">another link</a> and maybe <a href="https://google.com">one more</a> into the textarea.</p>
58+
<p>
59+
Or copy and paste a <a href="https://github.com">link</a> and
60+
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">another link</a> and maybe
61+
<a href="https://google.com">one more</a> into the textarea.
62+
</p>
5263

53-
<textarea cols="50" rows="10">The examples can be found here.</textarea>
64+
<textarea cols="50" rows="10">The examples can be found here.</textarea>
5465

55-
<script type="module">
56-
// import {subscribe} from '../dist/index.esm.js'
57-
import {subscribe} from 'https://unpkg.com/@github/paste-markdown/dist/index.esm.js'
58-
subscribe(document.querySelector('textarea'))
59-
</script>
60-
</body>
66+
<script type="module">
67+
import {subscribe} from '../dist/index.esm.js'
68+
// import {subscribe} from 'https://unpkg.com/@github/paste-markdown/dist/index.esm.js'
69+
subscribe(document.querySelector('textarea'))
70+
</script>
71+
</body>
6172
</html>

src/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import {install as installHTML, uninstall as uninstallHTML} from './paste-markdown-html'
22
import {install as installImageLink, uninstall as uninstallImageLink} from './paste-markdown-image-link'
33
import {install as installLink, uninstall as uninstallLink} from './paste-markdown-link'
4+
import {
5+
installAround as installSkipFormatting,
6+
uninstall as uninstallSkipFormatting
7+
} from './paste-keyboard-shortcut-helper'
48
import {install as installTable, uninstall as uninstallTable} from './paste-markdown-table'
59
import {install as installText, uninstall as uninstallText} from './paste-markdown-text'
610

@@ -9,14 +13,11 @@ interface Subscription {
913
}
1014

1115
function subscribe(el: HTMLElement): Subscription {
12-
installTable(el)
13-
installImageLink(el)
14-
installLink(el)
15-
installText(el)
16-
installHTML(el)
16+
installSkipFormatting(el, installTable, installImageLink, installLink, installText, installHTML)
1717

1818
return {
1919
unsubscribe: () => {
20+
uninstallSkipFormatting(el)
2021
uninstallTable(el)
2122
uninstallHTML(el)
2223
uninstallImageLink(el)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const skipFormattingMap = new WeakMap<HTMLElement, boolean>()
2+
3+
function setSkipFormattingFlag(event: KeyboardEvent): void {
4+
const {currentTarget: el} = event
5+
const isSkipFormattingKeys = event.code === 'KeyV' && (event.ctrlKey || event.metaKey) && event.shiftKey
6+
7+
// Supports Cmd+Shift+V (Chrome) / Cmd+Shift+Opt+V (Safari, Firefox and Edge) to mimic paste and match style shortcut on MacOS.
8+
if (isSkipFormattingKeys || (isSkipFormattingKeys && event.altKey)) {
9+
skipFormattingMap.set(el as HTMLElement, true)
10+
}
11+
}
12+
13+
function unsetSkipFormattedFlag(event: ClipboardEvent): void {
14+
const {currentTarget: el} = event
15+
skipFormattingMap.delete(el as HTMLElement)
16+
}
17+
18+
export function shouldSkipFormatting(el: HTMLElement): boolean {
19+
const shouldSkipFormattingState = skipFormattingMap.get(el) ?? false
20+
21+
return shouldSkipFormattingState
22+
}
23+
24+
export function installAround(el: HTMLElement, ...installCallbacks: Array<(el: HTMLElement) => void>): void {
25+
el.addEventListener('keydown', setSkipFormattingFlag)
26+
27+
for (const installCallback of installCallbacks) {
28+
installCallback(el)
29+
}
30+
31+
el.addEventListener('paste', unsetSkipFormattedFlag)
32+
}
33+
34+
export function uninstall(el: HTMLElement): void {
35+
el.removeEventListener('keydown', setSkipFormattingFlag)
36+
el.removeEventListener('paste', unsetSkipFormattedFlag)
37+
}

src/paste-markdown-html.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {insertText} from './text'
2+
import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper'
23

34
export function install(el: HTMLElement): void {
45
el.addEventListener('paste', onPaste)
@@ -10,6 +11,8 @@ export function uninstall(el: HTMLElement): void {
1011

1112
function onPaste(event: ClipboardEvent) {
1213
const transfer = event.clipboardData
14+
const {currentTarget: el} = event
15+
if (shouldSkipFormatting(el as HTMLElement)) return
1316
// if there is no clipboard data, or
1417
// if there is no html content in the clipboard, return
1518
if (!transfer || !hasHTML(transfer)) return

src/paste-markdown-image-link.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* @flow strict */
2-
32
import {insertText} from './text'
3+
import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper'
44

55
export function install(el: HTMLElement): void {
66
el.addEventListener('dragover', onDragover)
@@ -38,6 +38,9 @@ function onDragover(event: DragEvent) {
3838
}
3939

4040
function onPaste(event: ClipboardEvent) {
41+
const {currentTarget: el} = event
42+
if (shouldSkipFormatting(el as HTMLElement)) return
43+
4144
const transfer = event.clipboardData
4245
if (!transfer || !hasLink(transfer)) return
4346

src/paste-markdown-link.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {insertText} from './text'
2+
import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper'
23

34
export function install(el: HTMLElement): void {
45
el.addEventListener('paste', onPaste)
@@ -9,6 +10,9 @@ export function uninstall(el: HTMLElement): void {
910
}
1011

1112
function onPaste(event: ClipboardEvent) {
13+
const {currentTarget: el} = event
14+
if (shouldSkipFormatting(el as HTMLElement)) return
15+
1216
const transfer = event.clipboardData
1317
if (!transfer || !hasPlainText(transfer)) return
1418

src/paste-markdown-table.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {insertText} from './text'
2+
import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper'
23

34
export function install(el: HTMLElement): void {
45
el.addEventListener('dragover', onDragover)
@@ -35,6 +36,9 @@ function onDragover(event: DragEvent) {
3536
}
3637

3738
function onPaste(event: ClipboardEvent) {
39+
const {currentTarget: el} = event
40+
if (shouldSkipFormatting(el as HTMLElement)) return
41+
3842
if (!event.clipboardData) return
3943

4044
const textToPaste = generateText(event.clipboardData)

src/paste-markdown-text.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {insertText} from './text'
2+
import {shouldSkipFormatting} from './paste-keyboard-shortcut-helper'
23

34
export function install(el: HTMLElement): void {
45
el.addEventListener('paste', onPaste)
@@ -9,6 +10,9 @@ export function uninstall(el: HTMLElement): void {
910
}
1011

1112
function onPaste(event: ClipboardEvent) {
13+
const {currentTarget: el} = event
14+
if (shouldSkipFormatting(el as HTMLElement)) return
15+
1216
const transfer = event.clipboardData
1317
if (!transfer || !hasMarkdown(transfer)) return
1418

test/test.js

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import {subscribe} from '../dist/index.esm.js'
22

3+
const tableHtml = `
4+
<table>
5+
<thead><tr><th>name</th><th>origin</th></tr></thead>
6+
<tbody>
7+
<tr><td>hubot</td><td>github</td></tr>
8+
<tr><td>bender</td><td>futurama</td></tr>
9+
</tbody>
10+
</table>
11+
`
12+
13+
const tableMarkdown = 'name | origin\n-- | --\nhubot | github\nbender | futurama'
14+
315
describe('paste-markdown', function () {
416
describe('installed on textarea', function () {
517
let subscription
@@ -43,18 +55,10 @@ describe('paste-markdown', function () {
4355

4456
it('turns html tables into markdown', function () {
4557
const data = {
46-
'text/html': `
47-
<table>
48-
<thead><tr><th>name</th><th>origin</th></tr></thead>
49-
<tbody>
50-
<tr><td>hubot</td><td>github</td></tr>
51-
<tr><td>bender</td><td>futurama</td></tr>
52-
</tbody>
53-
</table>
54-
`
58+
'text/html': tableHtml
5559
}
5660
paste(textarea, data)
57-
assert.include(textarea.value, 'name | origin\n-- | --\nhubot | github\nbender | futurama')
61+
assert.include(textarea.value, tableMarkdown)
5862
})
5963

6064
it("doesn't execute JavaScript", async function () {
@@ -76,13 +80,7 @@ describe('paste-markdown', function () {
7680
const data = {
7781
'text/html': `
7882
<p>Here is a cool table</p>
79-
<table>
80-
<thead><tr><th>name</th><th>origin</th></tr></thead>
81-
<tbody>
82-
<tr><td>hubot</td><td>github</td></tr>
83-
<tr><td>bender</td><td>futurama</td></tr>
84-
</tbody>
85-
</table>
83+
${tableHtml}
8684
<p>Very cool</p>
8785
`
8886
}
@@ -91,7 +89,7 @@ describe('paste-markdown', function () {
9189
assert.equal(
9290
textarea.value.trim(),
9391
// eslint-disable-next-line github/unescaped-html-literal
94-
'<p>Here is a cool table</p>\n \n\nname | origin\n-- | --\nhubot | github\nbender | futurama\n\n\n <p>Very cool</p>'
92+
`<p>Here is a cool table</p>\n \n \n\n${tableMarkdown}\n\n\n\n <p>Very cool</p>`
9593
)
9694
})
9795

@@ -111,7 +109,7 @@ describe('paste-markdown', function () {
111109

112110
// Synthetic paste events don't manipulate the DOM. A empty textarea
113111
// means that the event handler didn't fire and normal paste happened.
114-
assert.equal(textarea.value, '')
112+
assertUnformattedPaste(textarea)
115113
})
116114

117115
it('accepts x-gfm', function () {
@@ -213,9 +211,45 @@ describe('paste-markdown', function () {
213211
paste(textarea, {'text/html': sentence, 'text/plain': plaintextSentence})
214212
assert.equal(textarea.value, markdownSentence)
215213
})
214+
215+
it('skip markdown formatting with (Ctrl+Shift+v)', function () {
216+
const data = {
217+
'text/html': tableHtml
218+
}
219+
220+
dispatchSkipFormattingKeyEvent(textarea)
221+
paste(textarea, data)
222+
assertUnformattedPaste(textarea)
223+
224+
textarea.value = ''
225+
paste(textarea, data)
226+
assert.include(textarea.value, tableMarkdown)
227+
})
216228
})
217229
})
218230

231+
/**
232+
* Note: It's possible to construct and dispatch a synthetic paste event,
233+
* but this will not affect the document's contents in tests to assert it.
234+
* https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event
235+
* So for that reason assert result on keydown (Ctrl+Shift+v) will be empty '' here.
236+
*/
237+
function assertUnformattedPaste(textarea) {
238+
return assert.equal(textarea.value, '')
239+
}
240+
241+
function dispatchSkipFormattingKeyEvent(textarea) {
242+
textarea.dispatchEvent(
243+
new KeyboardEvent('keydown', {
244+
key: 'v',
245+
code: 'KeyV',
246+
shiftKey: true,
247+
ctrlKey: true,
248+
metaKey: true
249+
})
250+
)
251+
}
252+
219253
function paste(textarea, data) {
220254
const dataTransfer = new DataTransfer()
221255
for (const key in data) {

0 commit comments

Comments
 (0)