|
| 1 | +import {insertText} from './text' |
| 2 | + |
| 3 | +export function install(el: HTMLElement): void { |
| 4 | + el.addEventListener('paste', onPaste) |
| 5 | +} |
| 6 | + |
| 7 | +export function uninstall(el: HTMLElement): void { |
| 8 | + el.removeEventListener('paste', onPaste) |
| 9 | +} |
| 10 | + |
| 11 | +type MarkdownTransformer = (element: HTMLElement | HTMLAnchorElement, args: string[]) => string |
| 12 | + |
| 13 | +function onPaste(event: ClipboardEvent) { |
| 14 | + const transfer = event.clipboardData |
| 15 | + if (!transfer || !hasHTML(transfer)) return |
| 16 | + |
| 17 | + const field = event.currentTarget |
| 18 | + if (!(field instanceof HTMLTextAreaElement)) return |
| 19 | + |
| 20 | + // Get the plaintext and html version of clipboard contents |
| 21 | + let text = transfer.getData('text/plain') |
| 22 | + const textHTML = transfer.getData('text/html') |
| 23 | + if (!textHTML) return |
| 24 | + |
| 25 | + text = text.trim() |
| 26 | + if (!text) return |
| 27 | + |
| 28 | + // Generate DOM tree from HTML string |
| 29 | + const parser = new DOMParser() |
| 30 | + const doc = parser.parseFromString(textHTML, 'text/html') |
| 31 | + |
| 32 | + const a = doc.getElementsByTagName('a') |
| 33 | + const markdown = transform(a, text, linkify as MarkdownTransformer) |
| 34 | + |
| 35 | + // If no changes made by transforming |
| 36 | + if (markdown === text) return |
| 37 | + |
| 38 | + event.stopPropagation() |
| 39 | + event.preventDefault() |
| 40 | + |
| 41 | + insertText(field, markdown) |
| 42 | +} |
| 43 | + |
| 44 | +// Build a markdown string from a DOM tree and plaintext |
| 45 | +function transform( |
| 46 | + elements: HTMLCollectionOf<HTMLElement>, |
| 47 | + text: string, |
| 48 | + transformer: MarkdownTransformer, |
| 49 | + ...args: string[] |
| 50 | +): string { |
| 51 | + const markdownParts = [] |
| 52 | + for (const element of elements) { |
| 53 | + const textContent = element.textContent || '' |
| 54 | + const {part, index} = trimAfter(text, textContent) |
| 55 | + markdownParts.push(part.replace(textContent, transformer(element, args))) |
| 56 | + text = text.slice(index) |
| 57 | + } |
| 58 | + markdownParts.push(text) |
| 59 | + return markdownParts.join('') |
| 60 | +} |
| 61 | + |
| 62 | +// Trim text at index of last character of the first occurrence of "search" and |
| 63 | +// return a new string with the substring until the index |
| 64 | +// Example: trimAfter('Hello world', 'world') => {part: 'Hello world', index: 11} |
| 65 | +// Example: trimAfter('Hello world', 'bananas') => {part: '', index: -1} |
| 66 | +function trimAfter(text: string, search = ''): {part: string; index: number} { |
| 67 | + let index = text.indexOf(search) |
| 68 | + if (index === -1) return {part: '', index} |
| 69 | + |
| 70 | + index += search.length |
| 71 | + |
| 72 | + return { |
| 73 | + part: text.substring(0, index), |
| 74 | + index |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +function hasHTML(transfer: DataTransfer): boolean { |
| 79 | + return transfer.types.includes('text/html') |
| 80 | +} |
| 81 | + |
| 82 | +function linkify(element: HTMLAnchorElement): string { |
| 83 | + return `[${element.textContent}](${element.href})` |
| 84 | +} |
0 commit comments