copyPaste.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /**
  2. * Creates a textarea that stays hidden on the page and gets focused when user presses CTRL while not having a form
  3. * input focused.
  4. * In future we may implement a better driver when better APIs are available.
  5. *
  6. * @constructor
  7. * @private
  8. */
  9. var instance;
  10. function copyPaste() {
  11. if (!instance) {
  12. instance = new CopyPasteClass();
  13. } else if (instance.hasBeenDestroyed()){
  14. instance.init();
  15. }
  16. instance.refCounter++;
  17. return instance;
  18. }
  19. if (typeof exports !== 'undefined') {
  20. module.exports = copyPaste;
  21. }
  22. function CopyPasteClass() {
  23. this.refCounter = 0;
  24. this.init();
  25. }
  26. CopyPasteClass.prototype.init = function () {
  27. var
  28. style,
  29. parent;
  30. this.copyCallbacks = [];
  31. this.cutCallbacks = [];
  32. this.pasteCallbacks = [];
  33. // this.listenerElement = document.documentElement;
  34. parent = document.body;
  35. if (document.getElementById('CopyPasteDiv')) {
  36. this.elDiv = document.getElementById('CopyPasteDiv');
  37. this.elTextarea = this.elDiv.firstChild;
  38. } else {
  39. this.elDiv = document.createElement('div');
  40. this.elDiv.id = 'CopyPasteDiv';
  41. style = this.elDiv.style;
  42. style.position = 'fixed';
  43. style.top = '-10000px';
  44. style.left = '-10000px';
  45. parent.appendChild(this.elDiv);
  46. this.elTextarea = document.createElement('textarea');
  47. this.elTextarea.className = 'copyPaste';
  48. this.elTextarea.onpaste = function(event) {
  49. var clipboardContents,
  50. temp;
  51. if ('WebkitAppearance' in document.documentElement.style) { // chrome and safari
  52. clipboardContents = event.clipboardData.getData("Text");
  53. // Safari adds an additional newline to copied text
  54. if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
  55. temp = clipboardContents.split('\n');
  56. if (temp[temp.length - 1] === '') {
  57. temp.pop();
  58. }
  59. clipboardContents = temp.join('\n');
  60. }
  61. this.value = clipboardContents;
  62. event.preventDefault();
  63. }
  64. };
  65. style = this.elTextarea.style;
  66. style.width = '10000px';
  67. style.height = '10000px';
  68. style.overflow = 'hidden';
  69. this.elDiv.appendChild(this.elTextarea);
  70. if (typeof style.opacity !== 'undefined') {
  71. style.opacity = 0;
  72. }
  73. }
  74. this.onKeyDownRef = this.onKeyDown.bind(this);
  75. document.documentElement.addEventListener('keydown', this.onKeyDownRef, false);
  76. };
  77. /**
  78. * Call method on every key down event
  79. *
  80. * @param {Event} event
  81. */
  82. CopyPasteClass.prototype.onKeyDown = function(event) {
  83. var _this = this,
  84. isCtrlDown = false;
  85. function isActiveElementEditable() {
  86. var element = document.activeElement;
  87. if (element.shadowRoot && element.shadowRoot.activeElement) {
  88. element = element.shadowRoot.activeElement;
  89. }
  90. return ['INPUT', 'SELECT', 'TEXTAREA'].indexOf(element.nodeName) > -1 || element.contentEditable === 'true';
  91. }
  92. // mac
  93. if (event.metaKey) {
  94. isCtrlDown = true;
  95. }
  96. // pc
  97. else if (event.ctrlKey && navigator.userAgent.indexOf('Mac') === -1) {
  98. isCtrlDown = true;
  99. }
  100. if (isCtrlDown) {
  101. // this is needed by fragmentSelection in Handsontable. Ignore copypaste.js behavior if fragment of cell text is selected
  102. if (document.activeElement !== this.elTextarea && (this.getSelectionText() !== '' || isActiveElementEditable())) {
  103. return;
  104. }
  105. this.selectNodeText(this.elTextarea);
  106. setTimeout(function() {
  107. if (document.activeElement !== _this.elTextarea) {
  108. _this.selectNodeText(_this.elTextarea);
  109. }
  110. }, 0);
  111. }
  112. if (event.isImmediatePropagationEnabled !== false && isCtrlDown &&
  113. (event.keyCode === 67 ||
  114. event.keyCode === 86 ||
  115. event.keyCode === 88)) {
  116. // works in all browsers, incl. Opera < 12.12
  117. if (event.keyCode === 88) {
  118. setTimeout(function () {
  119. _this.triggerCut(event);
  120. }, 0);
  121. } else if (event.keyCode === 86) {
  122. setTimeout(function () {
  123. _this.triggerPaste(event);
  124. }, 0);
  125. }
  126. }
  127. };
  128. //http://jsperf.com/textara-selection
  129. //http://stackoverflow.com/questions/1502385/how-can-i-make-this-code-work-in-ie
  130. /**
  131. * Select all text contains in passed node element
  132. *
  133. * @param {Element} element
  134. */
  135. CopyPasteClass.prototype.selectNodeText = function(element) {
  136. if (element) {
  137. element.select();
  138. }
  139. };
  140. //http://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text
  141. /**
  142. * Get selection text
  143. *
  144. * @returns {String}
  145. */
  146. CopyPasteClass.prototype.getSelectionText = function() {
  147. var text = '';
  148. if (window.getSelection) {
  149. text = window.getSelection().toString();
  150. } else if (document.selection && document.selection.type !== 'Control') {
  151. text = document.selection.createRange().text;
  152. }
  153. return text;
  154. };
  155. /**
  156. * Make string copyable
  157. *
  158. * @param {String} string
  159. */
  160. CopyPasteClass.prototype.copyable = function(string) {
  161. if (typeof string !== 'string' && string.toString === void 0) {
  162. throw new Error('copyable requires string parameter');
  163. }
  164. this.elTextarea.value = string;
  165. this.selectNodeText(this.elTextarea);
  166. };
  167. /*CopyPasteClass.prototype.onCopy = function (fn) {
  168. this.copyCallbacks.push(fn);
  169. };*/
  170. /**
  171. * Add function callback to onCut event
  172. *
  173. * @param {Function} callback
  174. */
  175. CopyPasteClass.prototype.onCut = function(callback) {
  176. this.cutCallbacks.push(callback);
  177. };
  178. /**
  179. * Add function callback to onPaste event
  180. *
  181. * @param {Function} callback
  182. */
  183. CopyPasteClass.prototype.onPaste = function(callback) {
  184. this.pasteCallbacks.push(callback);
  185. };
  186. /**
  187. * Remove callback from all events
  188. *
  189. * @param {Function} callback
  190. * @returns {Boolean}
  191. */
  192. CopyPasteClass.prototype.removeCallback = function(callback) {
  193. var i, len;
  194. for (i = 0, len = this.copyCallbacks.length; i < len; i++) {
  195. if (this.copyCallbacks[i] === callback) {
  196. this.copyCallbacks.splice(i, 1);
  197. return true;
  198. }
  199. }
  200. for (i = 0, len = this.cutCallbacks.length; i < len; i++) {
  201. if (this.cutCallbacks[i] === callback) {
  202. this.cutCallbacks.splice(i, 1);
  203. return true;
  204. }
  205. }
  206. for (i = 0, len = this.pasteCallbacks.length; i < len; i++) {
  207. if (this.pasteCallbacks[i] === callback) {
  208. this.pasteCallbacks.splice(i, 1);
  209. return true;
  210. }
  211. }
  212. return false;
  213. };
  214. /**
  215. * Trigger cut event
  216. *
  217. * @param {DOMEvent} event
  218. */
  219. CopyPasteClass.prototype.triggerCut = function(event) {
  220. var _this = this;
  221. if (_this.cutCallbacks) {
  222. setTimeout(function () {
  223. for (var i = 0, len = _this.cutCallbacks.length; i < len; i++) {
  224. _this.cutCallbacks[i](event);
  225. }
  226. }, 50);
  227. }
  228. };
  229. /**
  230. * Trigger paste event
  231. *
  232. * @param {DOMEvent} event
  233. * @param {String} string
  234. */
  235. CopyPasteClass.prototype.triggerPaste = function(event, string) {
  236. var _this = this;
  237. if (_this.pasteCallbacks) {
  238. setTimeout(function () {
  239. var val = string || _this.elTextarea.value;
  240. for (var i = 0, len = _this.pasteCallbacks.length; i < len; i++) {
  241. _this.pasteCallbacks[i](val, event);
  242. }
  243. }, 50);
  244. }
  245. };
  246. /**
  247. * Destroy instance
  248. */
  249. CopyPasteClass.prototype.destroy = function() {
  250. if (!this.hasBeenDestroyed() && --this.refCounter === 0) {
  251. if (this.elDiv && this.elDiv.parentNode) {
  252. this.elDiv.parentNode.removeChild(this.elDiv);
  253. this.elDiv = null;
  254. this.elTextarea = null;
  255. }
  256. document.documentElement.removeEventListener('keydown', this.onKeyDownRef);
  257. this.onKeyDownRef = null;
  258. }
  259. };
  260. /**
  261. * Check if instance has been destroyed
  262. *
  263. * @returns {Boolean}
  264. */
  265. CopyPasteClass.prototype.hasBeenDestroyed = function() {
  266. return !this.refCounter;
  267. };