All files / json-crdt-peritext-ui/events/clipboard DomClipboard.ts

4.08% Statements 4/98
0% Branches 0/36
0% Functions 0/11
4.59% Lines 4/87

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 1421x     1x   1x                                                                                                                                       1x                                                                                                                                        
import {saveSelection} from '../../dom/util';
import type {PeritextClipboard, PeritextClipboardData} from './types';
 
const toText = (buf: Uint8Array) => new TextDecoder().decode(buf);
 
const writeSync = (data: PeritextClipboardData<string>): boolean => {
  try {
    Iif (typeof document !== 'object') return false;
    const selection = window.getSelection();
    Iif (!selection) return false;
    const queryCommandSupported = document.queryCommandSupported;
    const copySupported = queryCommandSupported?.('copy') ?? true;
    const cutSupported = queryCommandSupported?.('cut') ?? true;
    Iif (!copySupported && !cutSupported) return false;
    const restoreSelection = saveSelection();
    const value = data['text/plain'] ?? '';
    const text = typeof value === 'string' ? value : '';
    const span = document.createElement('span');
    const style = span.style;
    style.whiteSpace = 'pre';
    style.userSelect = 'all';
    style.position = 'fixed';
    style.top = '-9999px';
    style.left = '-9999px';
    const listener = (event: ClipboardEvent) => {
      event.preventDefault();
      const clipboardData = event.clipboardData;
      Iif (!clipboardData) return;
      for (const type in data) {
        const value = data[type];
        switch (type) {
          case 'text/plain':
          case 'text/html':
          case 'image/png': {
            clipboardData.setData(type, value);
            break;
          }
          default: {
            clipboardData.setData('web ' + type, value);
          }
        }
      }
    };
    span.addEventListener('copy', listener);
    span.addEventListener('cut', listener);
    try {
      document.body.appendChild(span);
      const select = () => {
        span.textContent = text;
        selection.removeAllRanges();
        const range = document.createRange();
        range.selectNode(span);
        selection.addRange(range);
      };
      select();
      document.execCommand('cut');
      select();
      return document.execCommand('copy');
    } catch {
      return false;
    } finally {
      try {
        // span.removeEventListener('copy', listener);
        // span.removeEventListener('cut', listener);
        document.body.removeChild(span);
        restoreSelection?.();
      } catch {}
    }
  } catch {
    return false;
  }
};
 
export class DomClipboard implements PeritextClipboard {
  constructor(protected readonly clipboard: Clipboard) {}
 
  public writeText(text: string): undefined | Promise<void> {
    const success = writeSync({'text/plain': text});
    Iif (success) return;
    return this.clipboard.writeText(text);
  }
 
  public write(
    text: PeritextClipboardData<string>,
    binary?: PeritextClipboardData<Uint8Array>,
  ): undefined | Promise<void> {
    const success = writeSync(text);
    const binaryKeysLength = binary ? Object.keys(binary).length : 0;
    Iif (success && binaryKeysLength === 0) return;
    const clipboardData: Record<string, string | Blob> = {};
    const data = {
      ...binary,
      ...(!success ? text : {}),
    };
    for (const type in data) {
      switch (type) {
        case 'text/plain':
        case 'text/html':
        case 'image/png': {
          clipboardData[type] = new Blob([data[type]], {type});
          break;
        }
        default: {
          clipboardData['web ' + type] = new Blob([data[type]], {type});
        }
      }
    }
    const item = new ClipboardItem(clipboardData);
    const items: ClipboardItem[] = [item];
    return this.clipboard.write(items);
  }
 
  public async read<T extends string>(types: T[]): Promise<{[mime in T]: Uint8Array}> {
    const clipboard = this.clipboard;
    const items = await clipboard.read();
    const data = {} as {[mime in T]: Uint8Array};
    const promises: Promise<[type: T, value: Uint8Array]>[] = [];
    const item = items[0];
    for (const type of types) {
      Iif (item.types.includes(type))
        promises.push(
          item
            .getType(type)
            .then((blob) => blob.arrayBuffer())
            .then((value) => [type as T, new Uint8Array(value)]),
        );
    }
    const results = await Promise.all(promises);
    for (const [type, value] of results) data[type] = value;
    return data;
  }
 
  public async readData(): Promise<{text?: string; html?: string}> {
    const data: {text?: string; html?: string} = {};
    const {'text/plain': text, 'text/html': html} = await this.read(['text/plain', 'text/html']);
    Iif (!text && !html) return data;
    Iif (text) data.text = toText(text);
    Iif (html) data.html = toText(html);
    return data;
  }
}