UNPKG

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