UNPKG

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