530818559c01270bb7fec70a97ad10f031f51945c48a3b7cefc7a43be09499a8bd28b29cc4c12548707a0782763055ea2012d89d8aa6dbf1213384578a2b5f 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import select from 'select';
  2. /**
  3. * Inner class which performs selection from either `text` or `target`
  4. * properties and then executes copy or cut operations.
  5. */
  6. class ClipboardAction {
  7. /**
  8. * @param {Object} options
  9. */
  10. constructor(options) {
  11. this.resolveOptions(options);
  12. this.initSelection();
  13. }
  14. /**
  15. * Defines base properties passed from constructor.
  16. * @param {Object} options
  17. */
  18. resolveOptions(options = {}) {
  19. this.action = options.action;
  20. this.container = options.container;
  21. this.emitter = options.emitter;
  22. this.target = options.target;
  23. this.text = options.text;
  24. this.trigger = options.trigger;
  25. this.selectedText = '';
  26. }
  27. /**
  28. * Decides which selection strategy is going to be applied based
  29. * on the existence of `text` and `target` properties.
  30. */
  31. initSelection() {
  32. if (this.text) {
  33. this.selectFake();
  34. } else if (this.target) {
  35. this.selectTarget();
  36. }
  37. }
  38. /**
  39. * Creates a fake textarea element, sets its value from `text` property,
  40. */
  41. createFakeElement() {
  42. const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
  43. this.fakeElem = document.createElement('textarea');
  44. // Prevent zooming on iOS
  45. this.fakeElem.style.fontSize = '12pt';
  46. // Reset box model
  47. this.fakeElem.style.border = '0';
  48. this.fakeElem.style.padding = '0';
  49. this.fakeElem.style.margin = '0';
  50. // Move element out of screen horizontally
  51. this.fakeElem.style.position = 'absolute';
  52. this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';
  53. // Move element to the same position vertically
  54. let yPosition = window.pageYOffset || document.documentElement.scrollTop;
  55. this.fakeElem.style.top = `${yPosition}px`;
  56. this.fakeElem.setAttribute('readonly', '');
  57. this.fakeElem.value = this.text;
  58. return this.fakeElem;
  59. }
  60. /**
  61. * Get's the value of fakeElem,
  62. * and makes a selection on it.
  63. */
  64. selectFake() {
  65. const fakeElem = this.createFakeElement();
  66. this.fakeHandlerCallback = () => this.removeFake();
  67. this.fakeHandler =
  68. this.container.addEventListener('click', this.fakeHandlerCallback) ||
  69. true;
  70. this.container.appendChild(fakeElem);
  71. this.selectedText = select(fakeElem);
  72. this.copyText();
  73. this.removeFake();
  74. }
  75. /**
  76. * Only removes the fake element after another click event, that way
  77. * a user can hit `Ctrl+C` to copy because selection still exists.
  78. */
  79. removeFake() {
  80. if (this.fakeHandler) {
  81. this.container.removeEventListener('click', this.fakeHandlerCallback);
  82. this.fakeHandler = null;
  83. this.fakeHandlerCallback = null;
  84. }
  85. if (this.fakeElem) {
  86. this.container.removeChild(this.fakeElem);
  87. this.fakeElem = null;
  88. }
  89. }
  90. /**
  91. * Selects the content from element passed on `target` property.
  92. */
  93. selectTarget() {
  94. this.selectedText = select(this.target);
  95. this.copyText();
  96. }
  97. /**
  98. * Executes the copy operation based on the current selection.
  99. */
  100. copyText() {
  101. let succeeded;
  102. try {
  103. succeeded = document.execCommand(this.action);
  104. } catch (err) {
  105. succeeded = false;
  106. }
  107. this.handleResult(succeeded);
  108. }
  109. /**
  110. * Fires an event based on the copy operation result.
  111. * @param {Boolean} succeeded
  112. */
  113. handleResult(succeeded) {
  114. this.emitter.emit(succeeded ? 'success' : 'error', {
  115. action: this.action,
  116. text: this.selectedText,
  117. trigger: this.trigger,
  118. clearSelection: this.clearSelection.bind(this),
  119. });
  120. }
  121. /**
  122. * Moves focus away from `target` and back to the trigger, removes current selection.
  123. */
  124. clearSelection() {
  125. if (this.trigger) {
  126. this.trigger.focus();
  127. }
  128. document.activeElement.blur();
  129. window.getSelection().removeAllRanges();
  130. }
  131. /**
  132. * Sets the `action` to be performed which can be either 'copy' or 'cut'.
  133. * @param {String} action
  134. */
  135. set action(action = 'copy') {
  136. this._action = action;
  137. if (this._action !== 'copy' && this._action !== 'cut') {
  138. throw new Error('Invalid "action" value, use either "copy" or "cut"');
  139. }
  140. }
  141. /**
  142. * Gets the `action` property.
  143. * @return {String}
  144. */
  145. get action() {
  146. return this._action;
  147. }
  148. /**
  149. * Sets the `target` property using an element
  150. * that will be have its content copied.
  151. * @param {Element} target
  152. */
  153. set target(target) {
  154. if (target !== undefined) {
  155. if (target && typeof target === 'object' && target.nodeType === 1) {
  156. if (this.action === 'copy' && target.hasAttribute('disabled')) {
  157. throw new Error(
  158. 'Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute'
  159. );
  160. }
  161. if (
  162. this.action === 'cut' &&
  163. (target.hasAttribute('readonly') || target.hasAttribute('disabled'))
  164. ) {
  165. throw new Error(
  166. 'Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes'
  167. );
  168. }
  169. this._target = target;
  170. } else {
  171. throw new Error('Invalid "target" value, use a valid Element');
  172. }
  173. }
  174. }
  175. /**
  176. * Gets the `target` property.
  177. * @return {String|HTMLElement}
  178. */
  179. get target() {
  180. return this._target;
  181. }
  182. /**
  183. * Destroy lifecycle.
  184. */
  185. destroy() {
  186. this.removeFake();
  187. }
  188. }
  189. export default ClipboardAction;