]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/plugin/AbstractClipboard.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / plugin / AbstractClipboard.js
CommitLineData
6527f429
DM
1/**\r
2 * This base class manages clipboard data transfer for a component. As an abstract class,\r
3 * applications use derived classes such as `{@link Ext.grid.plugin.Clipboard}` instead\r
4 * and seldom use this class directly.\r
5 *\r
6 * ## Operation\r
7 *\r
8 * Components that interact with the clipboard do so in two directions: copy and paste.\r
9 * When copying to the clipboard, a component will often provide multiple data formats.\r
10 * On paste, the consumer of the data can then decide what format it prefers and ignore\r
11 * the others.\r
12 *\r
13 * ### Copy (and Cut)\r
14 *\r
15 * There are two storage locations provided for holding copied data:\r
16 *\r
17 * * The system clipboard, used to exchange data with other applications running\r
18 * outside the browser.\r
19 * * A memory space in the browser page that can hold data for use only by other\r
20 * components on the page. This allows for richer formats to be transferred.\r
21 *\r
22 * A component can copy (or cut) data in multiple formats as controlled by the\r
23 * `{@link #cfg-memory}` and `{@link #cfg-system}` configs.\r
24 *\r
25 * ### Paste\r
26 *\r
27 * While there may be many formats available, when a component is ready to paste, only\r
28 * one format can ultimately be used. This is specified by the `{@link #cfg-source}`\r
29 * config.\r
30 *\r
31 * ## Browser Limitations\r
32 *\r
33 * At the current time, browsers have only a limited ability to interact with the system\r
34 * clipboard. The only reliable, cross-browser, plugin-in-free technique for doing so is\r
35 * to use invisible elements and focus tricks **during** the processing of clipboard key\r
36 * presses like CTRL+C (on Windows/Linux) or CMD+C (on Mac).\r
37 *\r
38 * @protected\r
39 * @since 5.1.0\r
40 */\r
41Ext.define('Ext.plugin.AbstractClipboard', {\r
42 extend: 'Ext.plugin.Abstract',\r
43 requires: [\r
44 'Ext.util.KeyMap'\r
45 ],\r
46\r
47 cachedConfig: {\r
48 /**\r
49 * @cfg {Object} formats\r
50 * This object is keyed by the names of the data formats supported by this plugin.\r
51 * The property values of this object are objects with `get` and `put` properties\r
52 * that name the methods for getting data from (copy) and putting to into (paste)\r
53 * the associated component.\r
54 *\r
55 * For example:\r
56 *\r
57 * formats: {\r
58 * html: {\r
59 * get: 'getHtmlData',\r
60 * put: 'putHtmlData'\r
61 * }\r
62 * }\r
63 *\r
64 * This declares support for the "html" data format and indicates that the\r
65 * `getHtmlData` method should be called to copy HTML data from the component,\r
66 * while the `putHtmlData` should be called to paste HTML data into the component.\r
67 *\r
68 * By default, all derived classes must support a "text" format:\r
69 *\r
70 * formats: {\r
71 * text: {\r
72 * get: 'getTextData',\r
73 * put: 'putTextData'\r
74 * }\r
75 * }\r
76 *\r
77 * To understand the method signatures required to implement a data format, see the\r
78 * documentation for `{@link #getTextData}` and `{@link #putTextData}`.\r
79 *\r
80 * The format name "system" is not allowed.\r
81 *\r
82 * @protected\r
83 */\r
84 formats: {\r
85 text: {\r
86 get: 'getTextData',\r
87 put: 'putTextData'\r
88 }\r
89 }\r
90 },\r
91\r
92 config: {\r
93 /**\r
94 * @cfg {String/String[]} [memory]\r
95 * The data format(s) to copy to the private, memory clipboard. By default, data\r
96 * is not saved to the memory clipboard. Specify `true` to include all formats\r
97 * of data, or a string to copy a single format, or an array of strings to copy\r
98 * multiple formats.\r
99 */\r
100 memory: null,\r
101\r
102 /**\r
103 * @cfg {String/String[]} [source="system"]\r
104 * The format or formats in order of preference when pasting data. This list can\r
105 * be any of the valid formats, plus the name "system". When a paste occurs, this\r
106 * config is consulted. The first format specified by this config that has data\r
107 * available in the private memory space is used. If "system" is encountered in\r
108 * the list, whatever data is available on the system clipboard is chosen. At\r
109 * that point, no further source formats will be considered.\r
110 */\r
111 source: 'system',\r
112\r
113 /**\r
114 * @cfg {String} [system="text"]\r
115 * The data format to set in the system clipboard. By default, the "text"\r
116 * format is used. Based on the type of derived class, other formats may be\r
117 * possible.\r
118 */\r
119 system: 'text'\r
120 },\r
121\r
122 destroy: function () {\r
123 var me = this,\r
124 keyMap = me.keyMap,\r
125 shared = me.shared;\r
126\r
127 if (keyMap) {\r
128 // If we have a keyMap then we have incremented the shared usage counter\r
129 // and now need to remove ourselves.\r
130 me.keyMap = Ext.destroy(keyMap);\r
131 if (! --shared.counter) {\r
132 shared.textArea = Ext.destroy(shared.textArea);\r
133 }\r
134 } else {\r
135 // If we don't have a keyMap it is because we are waiting for the render\r
136 // event and haven't connected to the shared context.\r
137 me.renderListener = Ext.destroy(me.renderListener);\r
138 }\r
139\r
140 me.callParent();\r
141 },\r
142\r
143 init: function (comp) {\r
144 var me = this;\r
145\r
146 if (comp.rendered) {\r
147 this.finishInit(comp);\r
148 } else {\r
149 me.renderListener = comp.on({\r
150 render: function () {\r
151 me.renderListener = null;\r
152 me.finishInit(comp);\r
153 },\r
154 destroyable: true,\r
155 single: true\r
156 });\r
157 }\r
158 },\r
159\r
160 /**\r
161 * Returns the element target to listen to copy/paste.\r
162 *\r
163 * @param {Ext.Component} comp The component this plugin is initialized on.\r
164 * @return {Ext.dom.Element} The element target.\r
165 */\r
166 getTarget: function(comp) {\r
167 return comp.el;\r
168 },\r
169\r
170 /**\r
171 * This method returns the selected data in text format.\r
172 * @method getTextData\r
173 * @param {String} format The name of the format (i.e., "text").\r
174 * @param {Boolean} erase Pass `true` to erase (cut) the data, `false` to just copy.\r
175 * @return {String} The data in text format.\r
176 */\r
177\r
178 /**\r
179 * This method pastes the given text data.\r
180 * @method putTextData\r
181 * @param {Object} data The data in the indicated `format`.\r
182 * @param {String} format The name of the format (i.e., "text").\r
183 */\r
184\r
185 privates: {\r
186 /**\r
187 * @property {Object} shared\r
188 * The shared state for all clipboard-enabled components.\r
189 * @property {Number} shared.counter The number of clipboard-enabled components\r
190 * currently using this object.\r
191 * @property {Object} shared.data The clipboard data for intra-page copy/paste. The\r
192 * properties of the object are keyed by format.\r
193 * @property {Ext.dom.Element} textArea The shared textarea used to polyfill the\r
194 * lack of HTML5 clipboard API.\r
195 * @private\r
196 */\r
197 shared: {\r
198 counter: 0,\r
199\r
200 data: null,\r
201\r
202 textArea: null\r
203 },\r
204\r
205 applyMemory: function (value) {\r
206 // Same as "source" config but that allows "system" as a format.\r
207 value = this.applySource(value);\r
208\r
209 //<debug>\r
210 if (value) {\r
211 for (var i = value.length; i-- > 0; ) {\r
212 if (value[i] === 'system') {\r
213 Ext.raise('Invalid clipboard format "' + value[i] + '"');\r
214 }\r
215 }\r
216 }\r
217 //</debug>\r
218\r
219 return value;\r
220 },\r
221\r
222 applySource: function (value) {\r
223 // Make sure we have a non-empty String[] or null\r
224 if (value) {\r
225 if (Ext.isString(value)) {\r
226 value = [value];\r
227 } else if (value.length === 0) {\r
228 value = null;\r
229 }\r
230 }\r
231\r
232 //<debug>\r
233 if (value) {\r
234 var formats = this.getFormats();\r
235\r
236 for (var i = value.length; i-- > 0; ) {\r
237 if (value[i] !== 'system' && !formats[value[i]]) {\r
238 Ext.raise('Invalid clipboard format "' + value[i] + '"');\r
239 }\r
240 }\r
241 }\r
242 //</debug>\r
243\r
244 return value || null;\r
245 },\r
246\r
247 //<debug>\r
248 applySystem: function (value) {\r
249 var formats = this.getFormats();\r
250\r
251 if (!formats[value]) {\r
252 Ext.raise('Invalid clipboard format "' + value + '"');\r
253 }\r
254\r
255 return value;\r
256 },\r
257 //</debug>\r
258\r
259 doCutCopy: function (event, erase) {\r
260 var me = this,\r
261 formats = me.allFormats || me.syncFormats(),\r
262 data = me.getData(erase, formats),\r
263 memory = me.getMemory(),\r
264 system = me.getSystem(),\r
265 sys;\r
266\r
267 me.shared.data = memory && data;\r
268\r
269 if (system) {\r
270 sys = data[system];\r
271 if (formats[system] < 3) {\r
272 delete data[system];\r
273 }\r
274 me.setClipboardData(sys);\r
275 }\r
276 },\r
277\r
278 doPaste: function (format, data) {\r
279 var formats = this.getFormats();\r
280\r
281 this[formats[format].put](data, format);\r
282 },\r
283\r
284 finishInit: function (comp) {\r
285 var me = this;\r
286\r
287 me.keyMap = new Ext.util.KeyMap({\r
288 target: me.getTarget(comp),\r
289\r
290 binding: [{\r
291 ctrl: true, key: 'x', fn: me.onCut, scope: me\r
292 }, {\r
293 ctrl: true, key: 'c', fn: me.onCopy, scope: me\r
294 }, {\r
295 ctrl: true, key: 'v', fn: me.onPaste, scope: me\r
296 }]\r
297 });\r
298\r
299 ++me.shared.counter;\r
300\r
301 comp.on({\r
302 destroy: 'destroy',\r
303 scope: me\r
304 });\r
305 },\r
306\r
307 getData: function (erase, format) {\r
308 var me = this,\r
309 formats = me.getFormats(),\r
310 data, i, name, names;\r
311\r
312 if (Ext.isString(format)) {\r
313 //<debug>\r
314 if (!formats[format]) {\r
315 Ext.raise('Invalid clipboard format "' + format + '"');\r
316 }\r
317 //</debug>\r
318 data = me[formats[format].get](format, erase);\r
319 } else {\r
320 data = {};\r
321 names = [];\r
322\r
323 if (format) {\r
324 for (name in format) {\r
325 //<debug>\r
326 if (!formats[name]) {\r
327 Ext.raise('Invalid clipboard format "' + name + '"');\r
328 }\r
329 //</debug>\r
330 names.push(name);\r
331 }\r
332 } else {\r
333 names = Ext.Object.getAllKeys(formats);\r
334 }\r
335\r
336 for (i = names.length; i-- > 0; ) {\r
337 data[name] = me[formats[name].get](name, erase && !i);\r
338 }\r
339 }\r
340\r
341 return data;\r
342 },\r
343\r
344 /**\r
345 * @private\r
346 * @return {Ext.dom.Element}\r
347 */\r
348 getHiddenTextArea: function () {\r
349 var shared = this.shared,\r
350 el;\r
351 \r
352 el = shared.textArea;\r
353 \r
354 if (!el) {\r
355 el = shared.textArea = Ext.getBody().createChild({\r
356 tag: 'textarea',\r
357 tabIndex: -1, // don't tab through this fellow\r
358 style: {\r
359 position: 'absolute',\r
360 top: '-1000px',\r
361 width: '1px',\r
362 height: '1px'\r
363 }\r
364 });\r
365 \r
366 // We don't want this element to fire focus events ever\r
367 el.suspendFocusEvents();\r
368 }\r
369 \r
370 return el;\r
371 },\r
372\r
373 onCopy: function (keyCode, event) {\r
374 this.doCutCopy(event, false);\r
375 },\r
376\r
377 onCut: function (keyCode, event) {\r
378 this.doCutCopy(event, true);\r
379 },\r
380\r
381 onPaste: function (keyCode, event) {\r
382 var me = this,\r
383 sharedData = me.shared.data,\r
384 source = me.getSource(),\r
385 i, n, s;\r
386\r
387 if (source) {\r
388 for (i = 0, n = source.length; i < n; ++i) {\r
389 s = source[i];\r
390\r
391 if (s === 'system') {\r
392 // get the format used by the system clipboard.\r
393 s = me.getSystem();\r
394 me.pasteClipboardData(s);\r
395 break;\r
396 } else if (sharedData && (s in sharedData)) {\r
397 me.doPaste(s, sharedData[s]);\r
398 break;\r
399 }\r
400 }\r
401 }\r
402 },\r
403\r
404 pasteClipboardData: function(format) {\r
405 var me = this,\r
406 clippy = window.clipboardData,\r
407 area, focusEl;\r
408\r
409 if (clippy && clippy.getData) {\r
410 me.doPaste(format, clippy.getData("text"));\r
411 }\r
412 else {\r
413 focusEl = Ext.Element.getActiveElement(true);\r
414 area = me.getHiddenTextArea().dom;\r
415 area.value = '';\r
416\r
417 // We must not disturb application state by doing this focus\r
418 if (focusEl) {\r
419 focusEl.suspendFocusEvents();\r
420 }\r
421 \r
422 area.focus();\r
423\r
424 // Between now and the deferred function, the CTRL+V hotkey will have\r
425 // its default action processed which will paste the clipboard content\r
426 // into the textarea.\r
427 Ext.defer(function() {\r
428 // Focus back to the real destination\r
429 if (focusEl) {\r
430 focusEl.focus();\r
431 \r
432 // Restore framework focus handling\r
433 focusEl.resumeFocusEvents();\r
434 }\r
435 \r
436 me.doPaste(format, area.value);\r
437 area.value = '';\r
438 }, 100, me);\r
439 }\r
440 },\r
441\r
442 setClipboardData: function(data) {\r
443 var clippy = window.clipboardData;\r
444\r
445 if (clippy && clippy.setData) {\r
446 clippy.setData("text", data);\r
447 }\r
448 else {\r
449 var me = this,\r
450 area = me.getHiddenTextArea().dom,\r
451 focusEl = Ext.Element.getActiveElement(true);\r
452\r
453 area.value = data;\r
454\r
455 // We must not disturb application state by doing this focus\r
456 if (focusEl) {\r
457 focusEl.suspendFocusEvents();\r
458 }\r
459 \r
460 area.focus();\r
461 area.select();\r
462\r
463 // Between now and the deferred function, the CTRL+C/X hotkey will have\r
464 // its default action processed which will update the clipboard from the\r
465 // textarea.\r
466 Ext.defer(function() {\r
467 area.value = '';\r
468 \r
469 if (focusEl) {\r
470 focusEl.focus();\r
471 \r
472 // Restore framework focus handling\r
473 focusEl.resumeFocusEvents();\r
474 }\r
475 }, 50);\r
476 }\r
477 },\r
478\r
479 syncFormats: function () {\r
480 var me = this,\r
481 map = {},\r
482 memory = me.getMemory(),\r
483 system = me.getSystem(),\r
484 i, s;\r
485\r
486 if (system) {\r
487 map[system] = 1;\r
488 }\r
489\r
490 if (memory) {\r
491 for (i = memory.length; i-- > 0; ) {\r
492 s = memory[i];\r
493 map[s] = map[s] ? 3 : 2;\r
494 }\r
495 }\r
496\r
497 // 1: memory\r
498 // 2: system\r
499 // 3: both\r
500 return me.allFormats = map; // jshint ignore:line\r
501 },\r
502\r
503 updateMemory: function () {\r
504 this.allFormats = null;\r
505 },\r
506\r
507 updateSystem: function () {\r
508 this.allFormats = null;\r
509 }\r
510 }\r
511});\r