2
2
* Clipboard component manages the copy/cut/paste events by capturing these events from the browser,
3
3
* managing their state, and exposing an API to other components to get/set the data.
4
4
*
5
- * Because of a lack of standardization of ClipboardEvents between browsers, the way Clipboard
6
- * captures the events is by creating a hidden textarea element that's always focused with some text
7
- * selected. Here is a good write-up of this:
8
- * https://www.lucidchart.com/techblog/2014/12/02/definitive-guide-copying-pasting-javascript/
9
- *
10
5
* When ClipboardEvent is detected, Clipboard captures the event and calls the corresponding
11
6
* copy/cut/paste/input command actions, which will get called on the appropriate component.
12
7
*
8
+ * The Clipboard also handles triggering correctly the "input" command when any key is pressed.
9
+ *
13
10
* Usage:
14
11
* Components need to register copy/cut/paste actions with command.js:
15
12
* .copy() should return @pasteObj (defined below).
@@ -45,7 +42,6 @@ var {confirmModal} = require('app/client/ui2018/modals');
45
42
var { styled} = require ( 'grainjs' ) ;
46
43
47
44
var commands = require ( './commands' ) ;
48
- var dom = require ( '../lib/dom' ) ;
49
45
var Base = require ( './Base' ) ;
50
46
var tableUtil = require ( '../lib/tableUtil' ) ;
51
47
@@ -54,27 +50,10 @@ const t = makeT('Clipboard');
54
50
function Clipboard ( app ) {
55
51
Base . call ( this , null ) ;
56
52
this . _app = app ;
57
- this . copypasteField = this . autoDispose ( dom ( 'textarea.copypaste.mousetrap' , '' ) ) ;
58
- this . timeoutId = null ;
59
-
60
- this . onEvent ( this . copypasteField , 'input' , function ( elem , event ) {
61
- var value = elem . value ;
62
- elem . value = '' ;
63
- commands . allCommands . input . run ( value ) ;
64
- return false ;
65
- } ) ;
66
- this . onEvent ( this . copypasteField , 'copy' , this . _onCopy ) ;
67
- this . onEvent ( this . copypasteField , 'cut' , this . _onCut ) ;
68
- this . onEvent ( this . copypasteField , 'paste' , this . _onPaste ) ;
69
-
70
- document . body . appendChild ( this . copypasteField ) ;
71
53
72
54
FocusLayer . create ( this , {
73
- defaultFocusElem : this . copypasteField ,
74
- allowFocus : allowFocus ,
55
+ defaultFocusElem : document . body ,
75
56
onDefaultFocus : ( ) => {
76
- this . copypasteField . value = ' ' ;
77
- this . copypasteField . select ( ) ;
78
57
this . _app . trigger ( 'clipboard_focus' ) ;
79
58
} ,
80
59
onDefaultBlur : ( ) => {
@@ -94,6 +73,32 @@ function Clipboard(app) {
94
73
}
95
74
} ) ;
96
75
76
+ // Listen for copy/cut/paste events and trigger the corresponding clipboard action.
77
+ // Note that internally, before triggering the action, we check if the currently active element
78
+ // doesn't already handle these events itself.
79
+ // This allows to globally handle copy/cut/paste events, without impacting
80
+ // inputs/textareas/selects where copy/cut/paste events should be left alone.
81
+ this . onEvent ( document , 'copy' , ( _ , event ) => this . _onCopy ( event ) ) ;
82
+ this . onEvent ( document , 'cut' , ( _ , event ) => this . _onCut ( event ) ) ;
83
+ this . onEvent ( document , 'paste' , ( _ , event ) => this . _onPaste ( event ) ) ;
84
+
85
+ // when typing a random printable character while not focusing an interactive element,
86
+ // trigger the input command with it
87
+ // @TODO : there is currently an issue, sometimes when typing something, that makes us focus a cell textarea,
88
+ // and then we can mouseclick on a different cell: dom focus is still on textarea, visual table focus is on new cell.
89
+ this . onEvent ( document . body , 'keydown' , ( _ , event ) => {
90
+ if ( shouldAvoidClipboardShortcuts ( document . activeElement ) ) {
91
+ return ;
92
+ }
93
+ const ev = event . originalEvent ;
94
+ const collapsesWithCommands = commands . keypressCollapsesWithExistingCommand ( ev ) ;
95
+ const isPrintableCharacter = commands . keypressIsPrintableCharacter ( ev ) ;
96
+ if ( ! collapsesWithCommands && isPrintableCharacter ) {
97
+ commands . allCommands . input . run ( ev . key ) ;
98
+ event . preventDefault ( ) ;
99
+ }
100
+ } ) ;
101
+
97
102
// In the event of a cut a callback is provided by the viewsection that is the target of the cut.
98
103
// When called it returns the additional removal action needed for a cut.
99
104
this . _cutCallback = null ;
@@ -116,7 +121,10 @@ Clipboard.commands = {
116
121
* Internal helper fired on `copy` events. If a callback was registered from a component, calls the
117
122
* callback to get selection data and puts it on the clipboard.
118
123
*/
119
- Clipboard . prototype . _onCopy = function ( elem , event ) {
124
+ Clipboard . prototype . _onCopy = function ( event ) {
125
+ if ( shouldAvoidClipboardShortcuts ( document . activeElement ) ) {
126
+ return ;
127
+ }
120
128
event . preventDefault ( ) ;
121
129
122
130
let pasteObj = commands . allCommands . copy . run ( ) ;
@@ -136,7 +144,11 @@ Clipboard.prototype._doContextMenuCopyWithHeaders = function() {
136
144
this . _copyToClipboard ( pasteObj , 'copy' , true ) ;
137
145
} ;
138
146
139
- Clipboard . prototype . _onCut = function ( elem , event ) {
147
+ Clipboard . prototype . _onCut = function ( event ) {
148
+ if ( shouldAvoidClipboardShortcuts ( document . activeElement ) ) {
149
+ return ;
150
+ }
151
+
140
152
event . preventDefault ( ) ;
141
153
142
154
let pasteObj = commands . allCommands . cut . run ( ) ;
@@ -211,7 +223,11 @@ Clipboard.prototype._setCutCallback = function(pasteObj, cutData) {
211
223
* Internal helper fired on `paste` events. If a callback was registered from a component, calls the
212
224
* callback with data from the clipboard.
213
225
*/
214
- Clipboard . prototype . _onPaste = function ( elem , event ) {
226
+ Clipboard . prototype . _onPaste = function ( event ) {
227
+ if ( shouldAvoidClipboardShortcuts ( document . activeElement ) ) {
228
+ return ;
229
+ }
230
+
215
231
event . preventDefault ( ) ;
216
232
const cb = event . originalEvent . clipboardData ;
217
233
const plainText = cb . getData ( 'text/plain' ) ;
@@ -220,12 +236,6 @@ Clipboard.prototype._onPaste = function(elem, event) {
220
236
this . _doPaste ( pasteData , plainText ) ;
221
237
} ;
222
238
223
- var FOCUS_TARGET_TAGS = {
224
- 'INPUT' : true ,
225
- 'TEXTAREA' : true ,
226
- 'SELECT' : true ,
227
- 'IFRAME' : true ,
228
- } ;
229
239
230
240
Clipboard . prototype . _doContextMenuPaste = async function ( ) {
231
241
let clipboardItem ;
@@ -293,6 +303,17 @@ async function getTextFromClipboardItem(clipboardItem, type) {
293
303
}
294
304
}
295
305
306
+ const CLIPBOARD_TAGS = {
307
+ 'INPUT' : true ,
308
+ 'TEXTAREA' : true ,
309
+ 'SELECT' : true ,
310
+ } ;
311
+
312
+ const FOCUS_TARGET_TAGS = {
313
+ ...CLIPBOARD_TAGS ,
314
+ 'IFRAME' : true ,
315
+ } ;
316
+
296
317
/**
297
318
* Helper to determine if the currently active element deserves to keep its own focus, and capture
298
319
* copy-paste events. Besides inputs and textareas, any element can be marked to be a valid
@@ -304,6 +325,16 @@ function allowFocus(elem) {
304
325
elem . classList . contains ( 'clipboard_focus' ) ) ;
305
326
}
306
327
328
+ /**
329
+ * Helper to determine if the given element is a valid target for copy-cut-paste actions.
330
+ *
331
+ * It slightly differs from allowFocus: here we exclusively check for clipboard-related actions,
332
+ * not focus-related ones.
333
+ */
334
+ function shouldAvoidClipboardShortcuts ( elem ) {
335
+ return elem && CLIPBOARD_TAGS . hasOwnProperty ( elem . tagName )
336
+ }
337
+
307
338
Clipboard . allowFocus = allowFocus ;
308
339
309
340
function showUnavailableMenuCommandModal ( action ) {
0 commit comments