input.js revision 1d80c6a8d34f59543f7df1963c22d7efa292bcb0
1/*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2013 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
6 */
7
8/*jslint browser: true, white: false */
9/*global window, Util */
10
11var Keyboard, Mouse;
12
13(function () {
14    "use strict";
15
16    //
17    // Keyboard event handler
18    //
19
20    Keyboard = function (defaults) {
21        this._keyDownList = [];         // List of depressed keys
22                                        // (even if they are happy)
23
24        Util.set_defaults(this, defaults, {
25            'target': document,
26            'focused': true
27        });
28
29        // create the keyboard handler
30        this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(),
31            VerifyCharModifier( /* jshint newcap: false */
32                TrackKeyState(
33                    EscapeModifiers(this._handleRfbEvent.bind(this))
34                )
35            )
36        ); /* jshint newcap: true */
37
38        // keep these here so we can refer to them later
39        this._eventHandlers = {
40            'keyup': this._handleKeyUp.bind(this),
41            'keydown': this._handleKeyDown.bind(this),
42            'keypress': this._handleKeyPress.bind(this),
43            'blur': this._allKeysUp.bind(this)
44        };
45    };
46
47    Keyboard.prototype = {
48        // private methods
49
50        _handleRfbEvent: function (e) {
51            if (this._onKeyPress) {
52                Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") +
53                           ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")");
54                this._onKeyPress(e.keysym.keysym, e.type == 'keydown');
55            }
56        },
57
58        _handleKeyDown: function (e) {
59            if (!this._focused) { return true; }
60
61            if (this._handler.keydown(e)) {
62                // Suppress bubbling/default actions
63                Util.stopEvent(e);
64                return false;
65            } else {
66                // Allow the event to bubble and become a keyPress event which
67                // will have the character code translated
68                return true;
69            }
70        },
71
72        _handleKeyPress: function (e) {
73            if (!this._focused) { return true; }
74
75            if (this._handler.keypress(e)) {
76                // Suppress bubbling/default actions
77                Util.stopEvent(e);
78                return false;
79            } else {
80                // Allow the event to bubble and become a keyPress event which
81                // will have the character code translated
82                return true;
83            }
84        },
85
86        _handleKeyUp: function (e) {
87            if (!this._focused) { return true; }
88
89            if (this._handler.keyup(e)) {
90                // Suppress bubbling/default actions
91                Util.stopEvent(e);
92                return false;
93            } else {
94                // Allow the event to bubble and become a keyPress event which
95                // will have the character code translated
96                return true;
97            }
98        },
99
100        _allKeysUp: function () {
101            Util.Debug(">> Keyboard.allKeysUp");
102            this._handler.releaseAll();
103            Util.Debug("<< Keyboard.allKeysUp");
104        },
105
106        // Public methods
107
108        grab: function () {
109            //Util.Debug(">> Keyboard.grab");
110            var c = this._target;
111
112            Util.addEvent(c, 'keydown', this._eventHandlers.keydown);
113            Util.addEvent(c, 'keyup', this._eventHandlers.keyup);
114            Util.addEvent(c, 'keypress', this._eventHandlers.keypress);
115
116            // Release (key up) if window loses focus
117            Util.addEvent(window, 'blur', this._eventHandlers.blur);
118
119            //Util.Debug("<< Keyboard.grab");
120        },
121
122        ungrab: function () {
123            //Util.Debug(">> Keyboard.ungrab");
124            var c = this._target;
125
126            Util.removeEvent(c, 'keydown', this._eventHandlers.keydown);
127            Util.removeEvent(c, 'keyup', this._eventHandlers.keyup);
128            Util.removeEvent(c, 'keypress', this._eventHandlers.keypress);
129            Util.removeEvent(window, 'blur', this._eventHandlers.blur);
130
131            // Release (key up) all keys that are in a down state
132            this._allKeysUp();
133
134            //Util.Debug(">> Keyboard.ungrab");
135        },
136
137        sync: function (e) {
138            this._handler.syncModifiers(e);
139        }
140    };
141
142    Util.make_properties(Keyboard, [
143        ['target',     'wo', 'dom'],  // DOM element that captures keyboard input
144        ['focused',    'rw', 'bool'], // Capture and send key events
145
146        ['onKeyPress', 'rw', 'func'] // Handler for key press/release
147    ]);
148
149    //
150    // Mouse event handler
151    //
152
153    Mouse = function (defaults) {
154        this._mouseCaptured  = false;
155
156        this._doubleClickTimer = null;
157        this._lastTouchPos = null;
158
159        // Configuration attributes
160        Util.set_defaults(this, defaults, {
161            'target': document,
162            'focused': true,
163            'scale': 1.0,
164            'touchButton': 1
165        });
166
167        this._eventHandlers = {
168            'mousedown': this._handleMouseDown.bind(this),
169            'mouseup': this._handleMouseUp.bind(this),
170            'mousemove': this._handleMouseMove.bind(this),
171            'mousewheel': this._handleMouseWheel.bind(this),
172            'mousedisable': this._handleMouseDisable.bind(this)
173        };
174    };
175
176    Mouse.prototype = {
177        // private methods
178        _captureMouse: function () {
179            // capturing the mouse ensures we get the mouseup event
180            if (this._target.setCapture) {
181                this._target.setCapture();
182            }
183
184            // some browsers give us mouseup events regardless,
185            // so if we never captured the mouse, we can disregard the event
186            this._mouseCaptured = true;
187        },
188
189        _releaseMouse: function () {
190            if (this._target.releaseCapture) {
191                this._target.releaseCapture();
192            }
193            this._mouseCaptured = false;
194        },
195
196        _resetDoubleClickTimer: function () {
197            this._doubleClickTimer = null;
198        },
199
200        _handleMouseButton: function (e, down) {
201            if (!this._focused) { return true; }
202
203            if (this._notify) {
204                this._notify(e);
205            }
206
207            var evt = (e ? e : window.event);
208            var pos = Util.getEventPosition(e, this._target, this._scale);
209
210            var bmask;
211            if (e.touches || e.changedTouches) {
212                // Touch device
213
214                // When two touches occur within 500 ms of each other and are
215                // closer than 20 pixels together a double click is triggered.
216                if (down == 1) {
217                    if (this._doubleClickTimer === null) {
218                        this._lastTouchPos = pos;
219                    } else {
220                        clearTimeout(this._doubleClickTimer);
221
222                        // When the distance between the two touches is small enough
223                        // force the position of the latter touch to the position of
224                        // the first.
225
226                        var xs = this._lastTouchPos.x - pos.x;
227                        var ys = this._lastTouchPos.y - pos.y;
228                        var d = Math.sqrt((xs * xs) + (ys * ys));
229
230                        // The goal is to trigger on a certain physical width, the
231                        // devicePixelRatio brings us a bit closer but is not optimal.
232                        if (d < 20 * window.devicePixelRatio) {
233                            pos = this._lastTouchPos;
234                        }
235                    }
236                    this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
237                }
238                bmask = this._touchButton;
239                // If bmask is set
240            } else if (evt.which) {
241                /* everything except IE */
242                bmask = 1 << evt.button;
243            } else {
244                /* IE including 9 */
245                bmask = (evt.button & 0x1) +      // Left
246                        (evt.button & 0x2) * 2 +  // Right
247                        (evt.button & 0x4) / 2;   // Middle
248            }
249
250            if (this._onMouseButton) {
251                Util.Debug("onMouseButton " + (down ? "down" : "up") +
252                           ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
253                this._onMouseButton(pos.x, pos.y, down, bmask);
254            }
255            Util.stopEvent(e);
256            return false;
257        },
258
259        _handleMouseDown: function (e) {
260            this._captureMouse();
261            this._handleMouseButton(e, 1);
262        },
263
264        _handleMouseUp: function (e) {
265            if (!this._mouseCaptured) { return; }
266
267            this._handleMouseButton(e, 0);
268            this._releaseMouse();
269        },
270
271        _handleMouseWheel: function (e) {
272            if (!this._focused) { return true; }
273
274            if (this._notify) {
275                this._notify(e);
276            }
277
278            var evt = (e ? e : window.event);
279            var pos = Util.getEventPosition(e, this._target, this._scale);
280            var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40;
281            var bmask;
282            if (wheelData > 0) {
283                bmask = 1 << 3;
284            } else {
285                bmask = 1 << 4;
286            }
287
288            if (this._onMouseButton) {
289                this._onMouseButton(pos.x, pos.y, 1, bmask);
290                this._onMouseButton(pos.x, pos.y, 0, bmask);
291            }
292            Util.stopEvent(e);
293            return false;
294        },
295
296        _handleMouseMove: function (e) {
297            if (! this._focused) { return true; }
298
299            if (this._notify) {
300                this._notify(e);
301            }
302
303            var evt = (e ? e : window.event);
304            var pos = Util.getEventPosition(e, this._target, this._scale);
305            if (this._onMouseMove) {
306                this._onMouseMove(pos.x, pos.y);
307            }
308            Util.stopEvent(e);
309            return false;
310        },
311
312        _handleMouseDisable: function (e) {
313            if (!this._focused) { return true; }
314
315            var evt = (e ? e : window.event);
316            var pos = Util.getEventPosition(e, this._target, this._scale);
317
318            /* Stop propagation if inside canvas area */
319            if ((pos.realx >= 0) && (pos.realy >= 0) &&
320                (pos.realx < this._target.offsetWidth) &&
321                (pos.realy < this._target.offsetHeight)) {
322                //Util.Debug("mouse event disabled");
323                Util.stopEvent(e);
324                return false;
325            }
326
327            return true;
328        },
329
330
331        // Public methods
332        grab: function () {
333            var c = this._target;
334
335            if ('ontouchstart' in document.documentElement) {
336                Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown);
337                Util.addEvent(window, 'touchend', this._eventHandlers.mouseup);
338                Util.addEvent(c, 'touchend', this._eventHandlers.mouseup);
339                Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove);
340            } else {
341                Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown);
342                Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup);
343                Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup);
344                Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove);
345                Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
346                              this._eventHandlers.mousewheel);
347            }
348
349            /* Work around right and middle click browser behaviors */
350            Util.addEvent(document, 'click', this._eventHandlers.mousedisable);
351            Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
352        },
353
354        ungrab: function () {
355            var c = this._target;
356
357            if ('ontouchstart' in document.documentElement) {
358                Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown);
359                Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup);
360                Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup);
361                Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove);
362            } else {
363                Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown);
364                Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup);
365                Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup);
366                Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove);
367                Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
368                                 this._eventHandlers.mousewheel);
369            }
370
371            /* Work around right and middle click browser behaviors */
372            Util.removeEvent(document, 'click', this._eventHandlers.mousedisable);
373            Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
374
375        }
376    };
377
378    Util.make_properties(Mouse, [
379        ['target',         'ro', 'dom'],   // DOM element that captures mouse input
380        ['notify',         'ro', 'func'],  // Function to call to notify whenever a mouse event is received
381        ['focused',        'rw', 'bool'],  // Capture and send mouse clicks/movement
382        ['scale',          'rw', 'float'], // Viewport scale factor 0.0 - 1.0
383
384        ['onMouseButton',  'rw', 'func'],  // Handler for mouse button click/release
385        ['onMouseMove',    'rw', 'func'],  // Handler for mouse movement
386        ['touchButton',    'rw', 'int']    // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
387    ]);
388})();
389