Skip to content

Commit 625c969

Browse files
committed
WIP: clipboard
1 parent 3d22c43 commit 625c969

2 files changed

Lines changed: 116 additions & 4 deletions

File tree

src/lib/clipboard.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
export async function copyTableToClipboard(table: string[][]) {
2+
const textData = table.map(row => row.map(s => s.replace(/[\t\r\n]/g, '')).join('\t')).join('\n')
3+
4+
const htmlTable = document.createElement('table')
5+
for (const row of table) {
6+
const htmlRow = document.createElement('tr')
7+
htmlTable.appendChild(htmlRow)
8+
for (const cell of row) {
9+
const htmlCell = document.createElement('td')
10+
htmlRow.appendChild(htmlCell)
11+
htmlCell.innerText = cell
12+
}
13+
}
14+
const htmlData = htmlTable.outerHTML
15+
16+
const textBlob = new Blob([textData], { type: 'text/plain' })
17+
const htmlBlob = new Blob([htmlData], { type: 'text/html' })
18+
19+
const clipboardItem = new ClipboardItem({
20+
'text/plain': textBlob,
21+
'text/html': htmlBlob,
22+
})
23+
24+
await navigator.clipboard.write([clipboardItem])
25+
}
26+
27+
export async function pasteTableFromClipboard(): Promise<string[][] | null> {
28+
try {
29+
const clipboardItems = await navigator.clipboard.read()
30+
31+
for (const item of clipboardItems) {
32+
if (item.types.includes('text/html')) {
33+
const blob = await item.getType('text/html')
34+
const html = await blob.text()
35+
return parseHtmlTable(html)
36+
}
37+
}
38+
39+
for (const item of clipboardItems) {
40+
if (item.types.includes('text/plain')) {
41+
const blob = await item.getType('text/plain')
42+
const text = await blob.text()
43+
return parseTextTable(text)
44+
}
45+
}
46+
} catch (e) {
47+
// Ignore error, try readText
48+
}
49+
50+
try {
51+
const text = await navigator.clipboard.readText()
52+
return parseTextTable(text)
53+
} catch (e) {
54+
console.error('Failed to read clipboard', e)
55+
return null
56+
}
57+
}
58+
59+
function parseHtmlTable(html: string): string[][] {
60+
const parser = new DOMParser()
61+
const doc = parser.parseFromString(html, 'text/html')
62+
const table = doc.querySelector('table')
63+
64+
if (!table) return []
65+
66+
const data: string[][] = []
67+
for (const row of Array.from(table.rows)) {
68+
const rowData: string[] = []
69+
for (const cell of Array.from(row.cells)) {
70+
rowData.push(cell.innerText)
71+
}
72+
data.push(rowData)
73+
}
74+
return data
75+
}
76+
77+
function parseTextTable(text: string): string[][] {
78+
const rows = text.split(/\r\n|\n|\r/)
79+
if (rows[rows.length - 1] === '') {
80+
rows.pop()
81+
}
82+
return rows.map(row => row.split('\t'))
83+
}

src/table/Table.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { AddColumnButton } from './AddColumnButton'
1616
import { RowHeader } from './RowHeader'
1717
import { isEqual } from 'radashi'
1818
import { textContent } from 'src/components/CellContent'
19+
import { copyTableToClipboard, pasteTableFromClipboard } from 'src/lib/clipboard'
1920
import './Table.css'
2021

2122
const DEFAULT_COLUMN_SIZE = 80
@@ -443,17 +444,45 @@ export default function Table<Column, Value = unknown>(props: TableProps<Column,
443444
const range = activeRange()
444445
if (!range) return
445446

446-
props.onCopy?.(range.min, range.max)
447+
if (props.onCopy) {
448+
props.onCopy(range.min, range.max)
449+
} else {
450+
const table: string[][] = []
451+
for (let i = range.min[0]; i <= range.max[0]; i++) {
452+
const row: string[] = []
453+
for (let j = range.min[1]; j <= range.max[1]; j++) {
454+
row.push(String(props.getCellValue(i, props.columns[j]!)))
455+
}
456+
table.push(row)
457+
}
458+
copyTableToClipboard(table)
459+
}
447460
}
448461

449462
const handlePaste = async (ev?: ClipboardEvent) => {
450463
ev?.preventDefault()
451-
if (!props.rowsEditable) return
464+
// if (!props.rowsEditable) return
452465

453466
const range = activeRange()
454467
if (!range) return
455468

456-
props.onPaste?.(range.min, range.max)
469+
if (props.onPaste) {
470+
props.onPaste(range.min, range.max)
471+
} else {
472+
pasteTableFromClipboard().then(table => {
473+
if (!props.setCellValue) return
474+
if (!table) return
475+
let i = range.min[0]
476+
for (const row of table) {
477+
let j = range.min[1]
478+
for (const cell of row) {
479+
props.setCellValue(i, props.columns[j]!, cell as any)
480+
j += 1
481+
}
482+
i += 1
483+
}
484+
})
485+
}
457486
}
458487

459488
// Handle keyboard events
@@ -564,7 +593,7 @@ export default function Table<Column, Value = unknown>(props: TableProps<Column,
564593
onPointerUp={onCellUp}
565594
tabIndex={-1}
566595
>
567-
<div ref={focusEl} class="solid-tabular/focus-proxy" tabIndex={-1} contentEditable />
596+
<div ref={focusEl} class="solid-tabular/focus-proxy" tabIndex={-1} />
568597

569598
{/* Corner box */}
570599
<div class="solid-tabular/corner-box">

0 commit comments

Comments
 (0)