このコミットが含まれているのは:
2026-10-25 16:47:05 +09:00
コミット e71663bd22
16個のファイルの変更、523行の追加、89行の削除
+70
ファイルの表示
@@ -0,0 +1,70 @@
export type CountsClipboard = { text: string; html: string }
const escapeHTML = (value: string): string => value.replace (/[&<>"']/g, character =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]!)
export const countsClipboard = (rows: string[][], headers = false): CountsClipboard => {
// Excel accepts quoted TSV fields only when delimiters or quotes require them.
// Prefix formula-like text in both representations, including leading whitespace.
const safe = rows.map (row => row.map (value => /^\s*[=+@-]/.test (value)
? `'${value}` : value))
const text = safe.map (row => row.map (value => /[\t\r\n"]/.test (value)
? `"${value.replaceAll ('"', '""')}"` : value).join ('\t')).join ('\r\n')
const html = '<html><body><table>' + safe.map ((row, index) => '<tr>' + row.map (value => {
const tag = headers && index === 0 ? 'th' : 'td'
// Preserve time strings, leading zeroes, and user text instead of inferring dates/formulae.
const numeric = value.length <= 15 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test (value)
const style = numeric ? 'white-space:pre-wrap' : 'white-space:pre-wrap;mso-number-format:"\\@"'
return `<${tag} style="${escapeHTML (style)}">${escapeHTML (value)
.replace (/\r\n|\r|\n/g, '<br>')}</${tag}>`
}).join ('') + '</tr>').join ('') + '</table></body></html>'
return { text, html }
}
export const writeCountsClipboard = async (payload: CountsClipboard): Promise<void> => {
if (navigator.clipboard?.write != null && typeof ClipboardItem !== 'undefined')
{
try
{
await navigator.clipboard.write ([new ClipboardItem ({
'text/plain': new Blob ([payload.text], { type: 'text/plain' }),
'text/html': new Blob ([payload.html], { type: 'text/html' }) })])
return
}
catch
{
// Browsers without HTML clipboard support may still permit plain TSV.
}
}
if (navigator.clipboard?.writeText != null)
{
try
{
await navigator.clipboard.writeText (payload.text)
return
}
catch
{
// Try the synchronous copy event when clipboard permission is unavailable.
}
}
// The synchronous copy event fallback is also used on non-secure local origins.
const listener = (event: ClipboardEvent) => {
event.preventDefault ()
event.stopImmediatePropagation ()
event.clipboardData?.setData ('text/plain', payload.text)
event.clipboardData?.setData ('text/html', payload.html)
}
document.addEventListener ('copy', listener, true)
try
{
if (!document.execCommand ('copy')) throw new Error ('Clipboard unavailable')
}
finally
{
document.removeEventListener ('copy', listener, true)
}
}