From f42b57067d461f5a8c71deec9818670f0c71d818 Mon Sep 17 00:00:00 2001 From: Peder Johnsen Date: Mon, 29 May 2017 22:17:26 +0100 Subject: [PATCH] Container option (#368) * Allow container option to fix bugs related to bootstrap modals etc. * Updated readme to reflect addition of container option * Name link * Removed test log * Remove unwanted whitespace * Refactored description --- dist/clipboard.js | 19 ++++++++++++++----- dist/clipboard.min.js | 2 +- readme.md | 9 +++++++++ src/clipboard-action.js | 19 ++++++++++--------- src/clipboard.js | 18 ++++++++++-------- test/clipboard-action.js | 11 +++++++++++ test/clipboard.js | 14 ++++++++++++++ 7 files changed, 69 insertions(+), 23 deletions(-) diff --git a/dist/clipboard.js b/dist/clipboard.js index 75b6af3..f81e8fe 100644 --- a/dist/clipboard.js +++ b/dist/clipboard.js @@ -10,7 +10,7 @@ var DOCUMENT_NODE_TYPE = 9; /** * A polyfill for Element.matches() */ -if (typeof Element !== 'undefined' && !Element.prototype.matches) { +if (Element && !Element.prototype.matches) { var proto = Element.prototype; proto.matches = proto.matchesSelector || @@ -420,6 +420,7 @@ module.exports = E; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this.action = options.action; + this.container = options.container; this.emitter = options.emitter; this.target = options.target; this.text = options.text; @@ -448,7 +449,7 @@ module.exports = E; this.fakeHandlerCallback = function () { return _this.removeFake(); }; - this.fakeHandler = document.body.addEventListener('click', this.fakeHandlerCallback) || true; + this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true; this.fakeElem = document.createElement('textarea'); // Prevent zooming on iOS @@ -467,7 +468,7 @@ module.exports = E; this.fakeElem.setAttribute('readonly', ''); this.fakeElem.value = this.text; - document.body.appendChild(this.fakeElem); + this.container.appendChild(this.fakeElem); this.selectedText = (0, _select2.default)(this.fakeElem); this.copyText(); @@ -476,13 +477,13 @@ module.exports = E; key: 'removeFake', value: function removeFake() { if (this.fakeHandler) { - document.body.removeEventListener('click', this.fakeHandlerCallback); + this.container.removeEventListener('click', this.fakeHandlerCallback); this.fakeHandler = null; this.fakeHandlerCallback = null; } if (this.fakeElem) { - document.body.removeChild(this.fakeElem); + this.container.removeChild(this.fakeElem); this.fakeElem = null; } } @@ -601,6 +602,12 @@ module.exports = E; }; } + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); @@ -681,6 +688,7 @@ module.exports = E; this.action = typeof options.action === 'function' ? options.action : this.defaultAction; this.target = typeof options.target === 'function' ? options.target : this.defaultTarget; this.text = typeof options.text === 'function' ? options.text : this.defaultText; + this.container = _typeof(options.container) === 'object' ? options.container : document.body; } }, { key: 'listenClick', @@ -704,6 +712,7 @@ module.exports = E; action: this.action(trigger), target: this.target(trigger), text: this.text(trigger), + container: this.container, trigger: trigger, emitter: this }); diff --git a/dist/clipboard.min.js b/dist/clipboard.min.js index 1993676..1a9727d 100644 --- a/dist/clipboard.min.js +++ b/dist/clipboard.min.js @@ -4,4 +4,4 @@ * * Licensed MIT © Zeno Rocha */ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.Clipboard=e()}}(function(){var e,t,n;return function e(t,n,o){function i(a,c){if(!n[a]){if(!t[a]){var l="function"==typeof require&&require;if(!c&&l)return l(a,!0);if(r)return r(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var s=n[a]={exports:{}};t[a][0].call(s.exports,function(e){var n=t[a][1][e];return i(n?n:e)},s,s.exports,e,t,n,o)}return n[a].exports}for(var r="function"==typeof require&&require,a=0;a0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function e(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function e(){var t=this,n="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=document.body.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[n?"right":"left"]="-9999px";var o=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=o+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,document.body.appendChild(this.fakeElem),this.selectedText=(0,i.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function e(){this.fakeHandler&&(document.body.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(document.body.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function e(){this.selectedText=(0,i.default)(this.target),this.copyText()}},{key:"copyText",value:function e(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function e(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function e(){this.target&&this.target.blur(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function e(){this.removeFake()}},{key:"action",set:function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function e(){return this._action}},{key:"target",set:function e(t){if(void 0!==t){if(!t||"object"!==("undefined"==typeof t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function e(){return this._target}}]),e}();e.exports=c})},{select:5}],8:[function(t,n,o){!function(i,r){if("function"==typeof e&&e.amd)e(["module","./clipboard-action","tiny-emitter","good-listener"],r);else if("undefined"!=typeof o)r(n,t("./clipboard-action"),t("tiny-emitter"),t("good-listener"));else{var a={exports:{}};r(a,i.clipboardAction,i.tinyEmitter,i.goodListener),i.clipboard=a.exports}}(this,function(e,t,n,o){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function c(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function l(e,t){var n="data-clipboard-"+e;if(t.hasAttribute(n))return t.getAttribute(n)}var u=i(t),s=i(n),f=i(o),d=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText}},{key:"listenClick",value:function e(t){var n=this;this.listener=(0,f.default)(t,"click",function(e){return n.onClick(e)})}},{key:"onClick",value:function e(t){var n=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new u.default({action:this.action(n),target:this.target(n),text:this.text(n),trigger:n,emitter:this})}},{key:"defaultAction",value:function e(t){return l("action",t)}},{key:"defaultTarget",value:function e(t){var n=l("target",t);if(n)return document.querySelector(n)}},{key:"defaultText",value:function e(t){return l("text",t)}},{key:"destroy",value:function e(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],n="string"==typeof t?[t]:t,o=!!document.queryCommandSupported;return n.forEach(function(e){o=o&&!!document.queryCommandSupported(e)}),o}}]),t}(s.default);e.exports=h})},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)}); \ No newline at end of file +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,o){function i(a,c){if(!n[a]){if(!e[a]){var l="function"==typeof require&&require;if(!c&&l)return l(a,!0);if(r)return r(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var s=n[a]={exports:{}};e[a][0].call(s.exports,function(t){var n=e[a][1][t];return i(n?n:t)},s,s.exports,t,e,n,o)}return n[a].exports}for(var r="function"==typeof require&&require,a=0;a0&&void 0!==arguments[0]?arguments[0]:{};this.action=e.action,this.container=e.container,this.emitter=e.emitter,this.target=e.target,this.text=e.text,this.trigger=e.trigger,this.selectedText=""}},{key:"initSelection",value:function t(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function t(){var e=this,n="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return e.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[n?"right":"left"]="-9999px";var o=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=o+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,i.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function t(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function t(){this.selectedText=(0,i.default)(this.target),this.copyText()}},{key:"copyText",value:function t(){var e=void 0;try{e=document.execCommand(this.action)}catch(t){e=!1}this.handleResult(e)}},{key:"handleResult",value:function t(e){this.emitter.emit(e?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function t(){this.target&&this.target.blur(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function t(){this.removeFake()}},{key:"action",set:function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=e,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function t(){return this._action}},{key:"target",set:function t(e){if(void 0!==e){if(!e||"object"!==("undefined"==typeof e?"undefined":r(e))||1!==e.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&e.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(e.hasAttribute("readonly")||e.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=e}},get:function t(){return this._target}}]),t}();t.exports=c})},{select:5}],8:[function(e,n,o){!function(i,r){if("function"==typeof t&&t.amd)t(["module","./clipboard-action","tiny-emitter","good-listener"],r);else if("undefined"!=typeof o)r(n,e("./clipboard-action"),e("tiny-emitter"),e("good-listener"));else{var a={exports:{}};r(a,i.clipboardAction,i.tinyEmitter,i.goodListener),i.clipboard=a.exports}}(this,function(t,e,n,o){"use strict";function i(t){return t&&t.__esModule?t:{default:t}}function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function a(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function c(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function l(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}var u=i(e),s=i(n),f=i(o),d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},h=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof e.action?e.action:this.defaultAction,this.target="function"==typeof e.target?e.target:this.defaultTarget,this.text="function"==typeof e.text?e.text:this.defaultText,this.container="object"===d(e.container)?e.container:document.body}},{key:"listenClick",value:function t(e){var n=this;this.listener=(0,f.default)(e,"click",function(t){return n.onClick(t)})}},{key:"onClick",value:function t(e){var n=e.delegateTarget||e.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new u.default({action:this.action(n),target:this.target(n),text:this.text(n),container:this.container,trigger:n,emitter:this})}},{key:"defaultAction",value:function t(e){return l("action",e)}},{key:"defaultTarget",value:function t(e){var n=l("target",e);if(n)return document.querySelector(n)}},{key:"defaultText",value:function t(e){return l("text",e)}},{key:"destroy",value:function t(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],n="string"==typeof e?[e]:e,o=!!document.queryCommandSupported;return n.forEach(function(t){o=o&&!!document.queryCommandSupported(t)}),o}}]),e}(s.default);t.exports=p})},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)}); \ No newline at end of file diff --git a/readme.md b/readme.md index fb16bec..1dc1d89 100644 --- a/readme.md +++ b/readme.md @@ -145,6 +145,15 @@ new Clipboard('.btn', { }); ``` +For use in bootstrap modals or with any other library that changes the focus you'll want to set the focused element as the `container` value. +See [Issue #155 (comment)](https://github.com/zenorocha/clipboard.js/issues/155#issuecomment-273124130) + +```js +new Clipboard('.btn', { + container: document.getElementById('#modal') +}); +``` + Also, if you are working with single page apps, you may want to manage the lifecycle of the DOM more precisely. Here's how you clean up the events and objects that we create. ```js diff --git a/src/clipboard-action.js b/src/clipboard-action.js index aa5e3ad..ea2aa21 100644 --- a/src/clipboard-action.js +++ b/src/clipboard-action.js @@ -18,11 +18,12 @@ class ClipboardAction { * @param {Object} options */ resolveOptions(options = {}) { - this.action = options.action; - this.emitter = options.emitter; - this.target = options.target; - this.text = options.text; - this.trigger = options.trigger; + this.action = options.action; + this.container = options.container; + this.emitter = options.emitter; + this.target = options.target; + this.text = options.text; + this.trigger = options.trigger; this.selectedText = ''; } @@ -50,7 +51,7 @@ class ClipboardAction { this.removeFake(); this.fakeHandlerCallback = () => this.removeFake(); - this.fakeHandler = document.body.addEventListener('click', this.fakeHandlerCallback) || true; + this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true; this.fakeElem = document.createElement('textarea'); // Prevent zooming on iOS @@ -69,7 +70,7 @@ class ClipboardAction { this.fakeElem.setAttribute('readonly', ''); this.fakeElem.value = this.text; - document.body.appendChild(this.fakeElem); + this.container.appendChild(this.fakeElem); this.selectedText = select(this.fakeElem); this.copyText(); @@ -81,13 +82,13 @@ class ClipboardAction { */ removeFake() { if (this.fakeHandler) { - document.body.removeEventListener('click', this.fakeHandlerCallback); + this.container.removeEventListener('click', this.fakeHandlerCallback); this.fakeHandler = null; this.fakeHandlerCallback = null; } if (this.fakeElem) { - document.body.removeChild(this.fakeElem); + this.container.removeChild(this.fakeElem); this.fakeElem = null; } } diff --git a/src/clipboard.js b/src/clipboard.js index c8417fc..08ee3c8 100644 --- a/src/clipboard.js +++ b/src/clipboard.js @@ -24,9 +24,10 @@ class Clipboard extends Emitter { * @param {Object} options */ resolveOptions(options = {}) { - this.action = (typeof options.action === 'function') ? options.action : this.defaultAction; - this.target = (typeof options.target === 'function') ? options.target : this.defaultTarget; - this.text = (typeof options.text === 'function') ? options.text : this.defaultText; + this.action = (typeof options.action === 'function') ? options.action : this.defaultAction; + this.target = (typeof options.target === 'function') ? options.target : this.defaultTarget; + this.text = (typeof options.text === 'function') ? options.text : this.defaultText; + this.container = (typeof options.container === 'object') ? options.container : document.body; } /** @@ -49,11 +50,12 @@ class Clipboard extends Emitter { } this.clipboardAction = new ClipboardAction({ - action : this.action(trigger), - target : this.target(trigger), - text : this.text(trigger), - trigger, - emitter : this + action : this.action(trigger), + target : this.target(trigger), + text : this.text(trigger), + container : this.container, + trigger : trigger, + emitter : this }); } diff --git a/test/clipboard-action.js b/test/clipboard-action.js index d749233..8a3133c 100644 --- a/test/clipboard-action.js +++ b/test/clipboard-action.js @@ -22,10 +22,12 @@ describe('ClipboardAction', () => { it('should set base properties', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, text: 'foo' }); assert.property(clip, 'action'); + assert.property(clip, 'container'); assert.property(clip, 'emitter'); assert.property(clip, 'target'); assert.property(clip, 'text'); @@ -41,6 +43,7 @@ describe('ClipboardAction', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, text: 'foo' }); @@ -82,6 +85,7 @@ describe('ClipboardAction', () => { it('should create a fake element and select its value', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, text: 'blah' }); @@ -93,6 +97,7 @@ describe('ClipboardAction', () => { it('should remove a temporary fake element', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, text: 'blah' }); @@ -106,6 +111,7 @@ describe('ClipboardAction', () => { it('should select text from editable element', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, target: document.querySelector('#input') }); @@ -115,6 +121,7 @@ describe('ClipboardAction', () => { it('should select text from non-editable element', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, target: document.querySelector('#paragraph') }); @@ -166,6 +173,7 @@ describe('ClipboardAction', () => { it('should fire a success event with certain properties', done => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, target: document.querySelector('#input') }); @@ -184,6 +192,7 @@ describe('ClipboardAction', () => { it('should fire a error event with certain properties', done => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, target: document.querySelector('#input') }); @@ -203,6 +212,7 @@ describe('ClipboardAction', () => { it('should remove focus from target and text selection', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, target: document.querySelector('#input') }); @@ -220,6 +230,7 @@ describe('ClipboardAction', () => { it('should destroy an existing fake element', () => { let clip = new ClipboardAction({ emitter: new Emitter(), + container: document.body, text: 'blah' }); diff --git a/test/clipboard.js b/test/clipboard.js index 77e0214..9b54932 100644 --- a/test/clipboard.js +++ b/test/clipboard.js @@ -52,6 +52,20 @@ describe('Clipboard', () => { assert.equal(global.fn, clipboard.text); }); + + it('should set container as an object', () => { + let clipboard = new Clipboard('.btn', { + container: document.body + }); + + assert.equal(document.body, clipboard.container); + }); + + it('should set container as body by default', () => { + let clipboard = new Clipboard('.btn'); + + assert.equal(document.body, clipboard.container); + }); }); describe('#listenClick', () => {