clipboard-action.js 5.7 KB

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