Clipboard.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { getWindow } from '../misc/getWindow.js';
  2. import { readBlobText } from './Blob.js';
  3. import { createDataTransfer, getBlobFromDataTransferItem } from './DataTransfer.js';
  4. // Clipboard is not available in jsdom
  5. function _define_property(obj, key, value) {
  6. if (key in obj) {
  7. Object.defineProperty(obj, key, {
  8. value: value,
  9. enumerable: true,
  10. configurable: true,
  11. writable: true
  12. });
  13. } else {
  14. obj[key] = value;
  15. }
  16. return obj;
  17. }
  18. // MDN lists string|Blob|Promise<Blob|string> as possible types in ClipboardItemData
  19. // lib.dom.d.ts lists only Promise<Blob|string>
  20. // https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#syntax
  21. function createClipboardItem(window, ...blobs) {
  22. const dataMap = Object.fromEntries(blobs.map((b)=>[
  23. typeof b === 'string' ? 'text/plain' : b.type,
  24. Promise.resolve(b)
  25. ]));
  26. // use real ClipboardItem if available
  27. /* istanbul ignore if */ if (typeof window.ClipboardItem !== 'undefined') {
  28. return new window.ClipboardItem(dataMap);
  29. }
  30. return new class ClipboardItem {
  31. get types() {
  32. return Array.from(Object.keys(this.data));
  33. }
  34. async getType(type) {
  35. const value = await this.data[type];
  36. if (!value) {
  37. throw new Error(`${type} is not one of the available MIME types on this item.`);
  38. }
  39. return value instanceof window.Blob ? value : new window.Blob([
  40. value
  41. ], {
  42. type
  43. });
  44. }
  45. constructor(d){
  46. _define_property(this, "data", undefined);
  47. this.data = d;
  48. }
  49. }(dataMap);
  50. }
  51. const ClipboardStubControl = Symbol('Manage ClipboardSub');
  52. function createClipboardStub(window, control) {
  53. return Object.assign(new class Clipboard extends window.EventTarget {
  54. async read() {
  55. return Array.from(this.items);
  56. }
  57. async readText() {
  58. let text = '';
  59. for (const item of this.items){
  60. const type = item.types.includes('text/plain') ? 'text/plain' : item.types.find((t)=>t.startsWith('text/'));
  61. if (type) {
  62. text += await item.getType(type).then((b)=>readBlobText(b, window.FileReader));
  63. }
  64. }
  65. return text;
  66. }
  67. async write(data) {
  68. this.items = data;
  69. }
  70. async writeText(text) {
  71. this.items = [
  72. createClipboardItem(window, text)
  73. ];
  74. }
  75. constructor(...args){
  76. super(...args), _define_property(this, "items", []);
  77. }
  78. }(), {
  79. [ClipboardStubControl]: control
  80. });
  81. }
  82. function isClipboardStub(clipboard) {
  83. return !!(clipboard === null || clipboard === undefined ? undefined : clipboard[ClipboardStubControl]);
  84. }
  85. function attachClipboardStubToView(window) {
  86. if (isClipboardStub(window.navigator.clipboard)) {
  87. return window.navigator.clipboard[ClipboardStubControl];
  88. }
  89. const realClipboard = Object.getOwnPropertyDescriptor(window.navigator, 'clipboard');
  90. let stub;
  91. const control = {
  92. resetClipboardStub: ()=>{
  93. stub = createClipboardStub(window, control);
  94. },
  95. detachClipboardStub: ()=>{
  96. /* istanbul ignore if */ if (realClipboard) {
  97. Object.defineProperty(window.navigator, 'clipboard', realClipboard);
  98. } else {
  99. Object.defineProperty(window.navigator, 'clipboard', {
  100. value: undefined,
  101. configurable: true
  102. });
  103. }
  104. }
  105. };
  106. stub = createClipboardStub(window, control);
  107. Object.defineProperty(window.navigator, 'clipboard', {
  108. get: ()=>stub,
  109. configurable: true
  110. });
  111. return stub[ClipboardStubControl];
  112. }
  113. function resetClipboardStubOnView(window) {
  114. if (isClipboardStub(window.navigator.clipboard)) {
  115. window.navigator.clipboard[ClipboardStubControl].resetClipboardStub();
  116. }
  117. }
  118. function detachClipboardStubFromView(window) {
  119. if (isClipboardStub(window.navigator.clipboard)) {
  120. window.navigator.clipboard[ClipboardStubControl].detachClipboardStub();
  121. }
  122. }
  123. async function readDataTransferFromClipboard(document) {
  124. const window = document.defaultView;
  125. const clipboard = window === null || window === undefined ? undefined : window.navigator.clipboard;
  126. const items = clipboard && await clipboard.read();
  127. if (!items) {
  128. throw new Error('The Clipboard API is unavailable.');
  129. }
  130. const dt = createDataTransfer(window);
  131. for (const item of items){
  132. for (const type of item.types){
  133. dt.setData(type, await item.getType(type).then((b)=>readBlobText(b, window.FileReader)));
  134. }
  135. }
  136. return dt;
  137. }
  138. async function writeDataTransferToClipboard(document, clipboardData) {
  139. const window = getWindow(document);
  140. const clipboard = window.navigator.clipboard;
  141. const items = [];
  142. for(let i = 0; i < clipboardData.items.length; i++){
  143. const dtItem = clipboardData.items[i];
  144. const blob = await getBlobFromDataTransferItem(window, dtItem);
  145. items.push(createClipboardItem(window, blob));
  146. }
  147. const written = clipboard && await clipboard.write(items).then(()=>true, // Can happen with other implementations that e.g. require permissions
  148. /* istanbul ignore next */ ()=>false);
  149. if (!written) {
  150. throw new Error('The Clipboard API is unavailable.');
  151. }
  152. }
  153. const g = globalThis;
  154. /* istanbul ignore else */ if (typeof g.afterEach === 'function') {
  155. g.afterEach(()=>resetClipboardStubOnView(globalThis.window));
  156. }
  157. /* istanbul ignore else */ if (typeof g.afterAll === 'function') {
  158. g.afterAll(()=>detachClipboardStubFromView(globalThis.window));
  159. }
  160. export { attachClipboardStubToView, createClipboardItem, detachClipboardStubFromView, readDataTransferFromClipboard, resetClipboardStubOnView, writeDataTransferToClipboard };