diff --git a/package.json b/package.json index 34d20e1..7bf73f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@glokon/guacamole-common-js", - "version": "1.5.14", + "version": "1.5.15", "description": "Guacamole common js with typescript type definitions", "type": "module", "main": "lib/cjs/guacamole.js", diff --git a/src/KeyEventInterpreter.js b/src/KeyEventInterpreter.js index 44354fb..43e478c 100644 --- a/src/KeyEventInterpreter.js +++ b/src/KeyEventInterpreter.js @@ -20,84 +20,22 @@ var Guacamole = Guacamole || {}; /** - * An object that will accept raw key events and produce human readable text - * batches, seperated by at least `batchSeperation` milliseconds, which can be - * retrieved through the onbatch callback or by calling getCurrentBatch(). - * - * NOTE: The event processing logic and output format is based on the `guaclog` - * tool, with the addition of batching support. + * An object that will accept raw key events and produce a chronologically + * ordered array of key event objects. These events can be obtained by + * calling getEvents(). * * @constructor - * - * @param {number} [batchSeperation=5000] - * The minimum number of milliseconds that must elapse between subsequent - * batches of key-event-generated text. If 0 or negative, no splitting will - * occur, resulting in a single batch for all provided key events. - * * @param {number} [startTimestamp=0] * The starting timestamp for the recording being intepreted. If provided, * the timestamp of each intepreted event will be relative to this timestamp. * If not provided, the raw recording timestamp will be used. */ -Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, startTimestamp) { - - /** - * Reference to this Guacamole.KeyEventInterpreter. - * - * @private - * @type {!Guacamole.SessionRecording} - */ - var interpreter = this; - - // Default to 5 seconds if the batch seperation was not provided - if (batchSeperation === undefined || batchSeperation === null) - batchSeperation = 5000; +Guacamole.KeyEventInterpreter = function KeyEventInterpreter(startTimestamp) { // Default to 0 seconds to keep the raw timestamps if (startTimestamp === undefined || startTimestamp === null) startTimestamp = 0; - /** - * A definition for a known key. - * - * @constructor - * @private - * @param {KeyDefinition|object} [template={}] - * The object whose properties should be copied within the new - * KeyDefinition. - */ - var KeyDefinition = function KeyDefinition(template) { - - /** - * The X11 keysym of the key. - * @type {!number} - */ - this.keysym = parseInt(template.keysym); - - /** - * A human-readable name for the key. - * @type {!String} - */ - this.name = template.name; - - /** - * The value which would be typed in a typical text editor, if any. If the - * key is not associated with any typable value, or if the typable value is - * not generally useful in an auditing context, this will be undefined. - * @type {String} - */ - this.value = template.value; - - /** - * Whether this key is a modifier which may affect the interpretation of - * other keys, and thus should be tracked as it is held down. - * @type {!boolean} - * @default false - */ - this.modifier = template.modifier || false; - - }; - /** * A precursor array to the KNOWN_KEYS map. The objects contained within * will be constructed into full KeyDefinition objects. @@ -107,7 +45,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st * @type {Object[]} */ var _KNOWN_KEYS = [ - {keysym: 0xFE03, name: 'AltGr', value: "", modifier: true }, + {keysym: 0xFE03, name: 'AltGr' }, {keysym: 0xFF08, name: 'Backspace' }, {keysym: 0xFF09, name: 'Tab' }, {keysym: 0xFF0B, name: 'Clear' }, @@ -178,19 +116,19 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st {keysym: 0xFFD3, name: 'F22' }, {keysym: 0xFFD4, name: 'F23' }, {keysym: 0xFFD5, name: 'F24' }, - {keysym: 0xFFE1, name: 'Shift', value: "", modifier: true }, - {keysym: 0xFFE2, name: 'Shift', value: "", modifier: true }, - {keysym: 0xFFE3, name: 'Ctrl', value: null, modifier: true }, - {keysym: 0xFFE4, name: 'Ctrl', value: null, modifier: true }, + {keysym: 0xFFE1, name: 'Shift' }, + {keysym: 0xFFE2, name: 'Shift' }, + {keysym: 0xFFE3, name: 'Ctrl' }, + {keysym: 0xFFE4, name: 'Ctrl' }, {keysym: 0xFFE5, name: 'Caps' }, - {keysym: 0xFFE7, name: 'Meta', value: null, modifier: true }, - {keysym: 0xFFE8, name: 'Meta', value: null, modifier: true }, - {keysym: 0xFFE9, name: 'Alt', value: null, modifier: true }, - {keysym: 0xFFEA, name: 'Alt', value: null, modifier: true }, - {keysym: 0xFFEB, name: 'Super', value: null, modifier: true }, - {keysym: 0xFFEC, name: 'Super', value: null, modifier: true }, - {keysym: 0xFFED, name: 'Hyper', value: null, modifier: true }, - {keysym: 0xFFEE, name: 'Hyper', value: null, modifier: true }, + {keysym: 0xFFE7, name: 'Meta' }, + {keysym: 0xFFE8, name: 'Meta' }, + {keysym: 0xFFE9, name: 'Alt' }, + {keysym: 0xFFEA, name: 'Alt' }, + {keysym: 0xFFEB, name: 'Super' }, + {keysym: 0xFFEC, name: 'Super' }, + {keysym: 0xFFED, name: 'Hyper' }, + {keysym: 0xFFEE, name: 'Hyper' }, {keysym: 0xFFFF, name: 'Delete' } ]; @@ -205,60 +143,18 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st _KNOWN_KEYS.forEach(function createKeyDefinitionMap(keyDefinition) { // Construct a map of keysym to KeyDefinition object - KNOWN_KEYS[keyDefinition.keysym] = new KeyDefinition(keyDefinition) + KNOWN_KEYS[keyDefinition.keysym] = ( + new Guacamole.KeyEventInterpreter.KeyDefinition(keyDefinition)); }); /** - * A map of X11 keysyms to a KeyDefinition object, if the corresponding - * key is currently pressed. If a keysym has no entry in this map at all, - * it means that the key is not being pressed. Note that not all keysyms - * are necessarily tracked within this map - only those that are explicitly - * tracked. - * - * @private - * @type {Object. } - */ - var pressedKeys = {}; - - /** - * The current key event batch, containing a representation of all key - * events processed since the end of the last batch passed to onbatch. - * Null if no key events have been processed yet. + * All key events parsed as of the most recent handleKeyEvent() invocation. * * @private - * @type {!KeyEventBatch} - */ - var currentBatch = null; - - /** - * The timestamp of the most recent key event processed. - * - * @private - * @type {Number} - */ - var lastKeyEvent = 0; - - /** - * Returns true if the currently-pressed keys are part of a shortcut, or - * false otherwise. - * - * @private - * @returns {!boolean} - * True if the currently-pressed keys are part of a shortcut, or false - * otherwise. + * @type {!Guacamole.KeyEventInterpreter.KeyEvent[]} */ - function isShortcut() { - - // If one of the currently-pressed keys is non-printable, a shortcut - // is being typed - for (var keysym in pressedKeys) { - if (pressedKeys[keysym].value === null) - return true; - } - - return false; - } + var parsedEvents = []; /** * If the provided keysym corresponds to a valid UTF-8 character, return @@ -268,7 +164,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st * @param {Number} keysym * The keysym to produce a UTF-8 KeyDefinition for, if valid. * - * @returns {KeyDefinition} + * @returns {Guacamole.KeyEventInterpreter.KeyDefinition} * A KeyDefinition for the provided keysym, if it's a valid UTF-8 * keysym, or null otherwise. */ @@ -283,7 +179,8 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st var name = String.fromCharCode(codepoint); // Create and return the definition - return new KeyDefinition({keysym: keysym, name: name, value: name, modifier: false}); + return new Guacamole.KeyEventInterpreter.KeyDefinition({ + keysym: keysym, name: name, value: name}); } @@ -310,7 +207,7 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st // If it's not UTF-8, return an unknown definition, with the name // just set to the hex value of the keysym - return new KeyDefinition({ + return new Guacamole.KeyEventInterpreter.KeyDefinition({ keysym: keysym, name: '0x' + String(keysym.toString(16)) }) @@ -318,20 +215,8 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st } /** - * Fired whenever a new batch of typed text extracted from key events - * is available. A new batch will be provided every time a new key event - * is processed after more than batchSeperation milliseconds after the - * previous key event. - * - * @event - * @param {!Guacamole.KeyEventInterpreter.KeyEventBatch} - */ - this.onbatch = null; - - /** - * Handles a raw key event, potentially appending typed text to the - * current batch, and calling onbatch with the current batch, if the - * callback is set and a new batch is about to be started. + * Handles a raw key event, appending a new key event object for every + * handled raw event. * * @param {!string[]} args * The arguments of the key event. @@ -347,111 +232,18 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st // The timestamp when this key event occured var timestamp = parseInt(args[2]); - // If no current batch exists, start a new one now - if (!currentBatch) - currentBatch = new Guacamole.KeyEventInterpreter.KeyEventBatch(); - - // Only switch to a new batch of text if sufficient time has passed - // since the last key event - var newBatch = (batchSeperation >= 0 - && (timestamp - lastKeyEvent) >= batchSeperation); - lastKeyEvent = timestamp; - - if (newBatch) { - - // Call the handler with the current batch of text and the timestamp - // at which the current batch started - if (currentBatch.events.length && interpreter.onbatch) - interpreter.onbatch(currentBatch); - - // Move on to the next batch of text - currentBatch = new Guacamole.KeyEventInterpreter.KeyEventBatch(); - - } - - var keyDefinition = getKeyDefinitionByKeysym(keysym); - - // Mark down whether the key was pressed or released - if (keyDefinition.modifier) { - if (pressed) - pressedKeys[keysym] = keyDefinition; - else - delete pressedKeys[keysym]; - } - - // Append to the current typed value when a printable - // (non-modifier) key is pressed - else if (pressed) { - - var relativeTimestap = timestamp - startTimestamp; + // The timestamp relative to the provided initial timestamp + var relativeTimestap = timestamp - startTimestamp; - if (isShortcut()) { + // Known information about the parsed key + var definition = getKeyDefinitionByKeysym(keysym); - var shortcutText = '<'; - - var firstKey = true; - - // Compose entry by inspecting the state of each tracked key. - // At least one key must be pressed when in a shortcut. - for (var keysym in pressedKeys) { - - var pressedKeyDefinition = pressedKeys[keysym]; - - // Print name of key - if (firstKey) { - shortcutText += pressedKeyDefinition.name; - firstKey = false; - } - - else - shortcutText += ('+' + pressedKeyDefinition.name); - - } - - // Finally, append the printable key to close the shortcut - shortcutText += ('+' + keyDefinition.name + '>') - - // Add the shortcut to the current batch - currentBatch.simpleValue += shortcutText; - currentBatch.events.push(new Guacamole.KeyEventInterpreter.KeyEvent( - shortcutText, false, relativeTimestap)); - - } - - // Print the key itself - else { - - var keyText; - var typed; - - // Print the value if explicitly defined - if (keyDefinition.value != null) { - - keyText = keyDefinition.value; - typed = true; - - } - - // Otherwise print the name - else { - - keyText = ('<' + keyDefinition.name + '>'); - - // While this is a representation for a single character, - // the key text is the name of the key, not the actual - // character itself - typed = false; - - } - - // Add the key to the current batch - currentBatch.simpleValue += keyText; - currentBatch.events.push(new Guacamole.KeyEventInterpreter.KeyEvent( - keyText, typed, relativeTimestap)); - - } - - } + // Push the latest parsed event into the list + parsedEvents.push(new Guacamole.KeyEventInterpreter.KeyEvent({ + definition: definition, + pressed: pressed, + timestamp: relativeTimestap + })); }; @@ -460,91 +252,84 @@ Guacamole.KeyEventInterpreter = function KeyEventInterpreter(batchSeperation, st * incomplete, as more key events might be processed before the next * batch starts. * - * @returns {Guacamole.KeyEventInterpreter.KeyEventBatch} + * @returns {Guacamole.KeyEventInterpreter.KeyEvent[]} * The current batch of text. */ - this.getCurrentBatch = function getCurrentBatch() { - return currentBatch; + this.getEvents = function getEvents() { + return parsedEvents; }; + }; /** - * A granular description of an extracted key event, including a human-readable - * text representation of the event, whether the event is directly typed or not, - * and the timestamp when the event occured. + * A definition for a known key. * * @constructor - * @param {!String} text - * A human-readable representation of the event. - * - * @param {!boolean} typed - * True if this event represents a directly-typed character, or false - * otherwise. - * - * @param {!Number} timestamp - * The timestamp from the recording when this event occured. + * @param {Guacamole.KeyEventInterpreter.KeyDefinition|object} [template={}] + * The object whose properties should be copied within the new + * KeyDefinition. */ -Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(text, typed, timestamp) { +Guacamole.KeyEventInterpreter.KeyDefinition = function KeyDefinition(template) { + + // Use empty object by default + template = template || {}; /** - * A human-readable representation of the event. If a printable character - * was directly typed, this will just be that character. Otherwise it will - * be a string describing the event. - * - * @type {!String} + * The X11 keysym of the key. + * @type {!number} */ - this.text = text; + this.keysym = parseInt(template.keysym); /** - * True if this text of this event is exactly a typed character, or false - * otherwise. - * - * @type {!boolean} + * A human-readable name for the key. + * @type {!String} */ - this.typed = typed; + this.name = template.name; /** - * The timestamp from the recording when this event occured. If a - * `startTimestamp` value was provided to the interpreter constructor, this - * will be relative to start of the recording. If not, it will be the raw - * timestamp from the key event. - * - * @type {!Number} + * The value which would be typed in a typical text editor, if any. If the + * key is not associated with any typeable value, this will be undefined. + * @type {String} */ - this.timestamp = timestamp; + this.value = template.value; }; /** - * A series of intepreted key events, seperated by at least the configured - * batchSeperation value from any other key events in the recording corresponding - * to the interpreted key events. A batch will always consist of at least one key - * event, and an associated simplified representation of the event(s). + * A granular description of an extracted key event, including a human-readable + * text representation of the event, whether the event is directly typed or not, + * and the timestamp when the event occured. * * @constructor - * @param {!Guacamole.KeyEventInterpreter.KeyEvent[]} events - * The interpreted key events for this batch. - * - * @param {!String} simpleValue - * The simplified, human-readable value representing the key events for - * this batch. + * @param {Guacamole.KeyEventInterpreter.KeyEvent|object} [template={}] + * The object whose properties should be copied within the new + * KeyEvent. */ -Guacamole.KeyEventInterpreter.KeyEventBatch = function KeyEventBatch(events, simpleValue) { +Guacamole.KeyEventInterpreter.KeyEvent = function KeyEvent(template) { + + // Use empty object by default + template = template || {}; /** - * All key events for this batch. + * The key definition for the pressed key. * - * @type {!Guacamole.KeyEventInterpreter.KeyEvent[]} + * @type {!Guacamole.KeyEventInterpreter.KeyDefinition} */ - this.events = events || []; + this.definition = template.definition; /** - * The simplified, human-readable value representing the key events for - * this batch, equivalent to concatenating the `text` field of all key - * events in the batch. + * True if the key was pressed to create this event, or false if it was + * released. * - * @type {!String} + * @type {!boolean} + */ + this.pressed = !!template.pressed; + + /** + * The timestamp from the recording when this event occured. + * + * @type {!Number} */ - this.simpleValue = simpleValue || ''; + this.timestamp = template.timestamp; }; diff --git a/src/Mouse.js b/src/Mouse.js index 2f8bc28..c032201 100644 --- a/src/Mouse.js +++ b/src/Mouse.js @@ -218,7 +218,7 @@ Guacamole.Mouse = function Mouse(element) { // Otherwise, assume legacy mousewheel event and line scrolling else delta = e.detail * guac_mouse.PIXELS_PER_LINE; - + // Update overall delta scroll_delta += delta; @@ -258,9 +258,16 @@ Guacamole.Mouse = function Mouse(element) { } - element.addEventListener('DOMMouseScroll', mousewheel_handler, false); - element.addEventListener('mousewheel', mousewheel_handler, false); - element.addEventListener('wheel', mousewheel_handler, false); + if (window.WheelEvent) { + // All modern browsers support wheel events. + element.addEventListener('wheel', mousewheel_handler, false); + } + else { + // Legacy FireFox wheel events. + element.addEventListener('DOMMouseScroll', mousewheel_handler, false); + // Legacy Chrome/IE/other wheel events. + element.addEventListener('mousewheel', mousewheel_handler, false); + } /** * Whether the browser supports CSS3 cursor styling, including hotspot @@ -299,7 +306,7 @@ Guacamole.Mouse = function Mouse(element) { * hotspot coordinates. This affects styling of the element backing this * Guacamole.Mouse only, and may fail depending on browser support for * setting the mouse cursor. - * + * * If setting the local cursor is desired, it is up to the implementation * to do something else, such as use the software cursor built into * Guacamole.Display, if the local cursor cannot be set. @@ -434,7 +441,7 @@ Guacamole.Mouse.State = function State(template) { this.up = template.up || false; /** - * Whether the down mouse button is currently pressed. This is the fifth + * Whether the down mouse button is currently pressed. This is the fifth * mouse button, associated with downward scrolling of the mouse scroll * wheel. * @@ -447,7 +454,7 @@ Guacamole.Mouse.State = function State(template) { /** * All mouse buttons that may be represented by a - * {@link Guacamole.Mouse.State}. + * {@link Guacamole.Mouse.State}. * * @readonly * @enum @@ -515,7 +522,7 @@ Guacamole.Mouse.State.Buttons = { * * @param {!Guacamole.Mouse.State} state * The current mouse state. - * + * * @param {Event|Event[]} [events=[]] * The DOM events that are related to this event, if any. */ @@ -564,7 +571,7 @@ Guacamole.Mouse.Event = function MouseEvent(type, state, events) { * generated (using functions like [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch}, * [press()]{@link Guacamole.Mouse.Event.Target#press}, and * [release()]{@link Guacamole.Mouse.Event.Target#release}). - * + * * @constructor * @augments Guacamole.Event.Target */ @@ -759,10 +766,10 @@ Guacamole.Mouse.Event.Target = function MouseEventTarget() { /** * Provides cross-browser relative touch event translation for a given element. - * + * * Touch events are translated into mouse events as if the touches occurred * on a touchpad (drag to push the mouse pointer, tap to click). - * + * * @example * var touchpad = new Guacamole.Mouse.Touchpad(client.getDisplay().getElement()); * @@ -831,7 +838,7 @@ Guacamole.Mouse.Touchpad = function Touchpad(element) { * The current mouse state. The properties of this state are updated when * mouse events fire. This state object is also passed in as a parameter to * the handler of any mouse events. - * + * * @type {!Guacamole.Mouse.State} */ this.currentState = new Guacamole.Mouse.State(); @@ -852,12 +859,12 @@ Guacamole.Mouse.Touchpad = function Touchpad(element) { var click_release_timeout = null; element.addEventListener("touchend", function(e) { - + e.preventDefault(); - + // If we're handling a gesture AND this is the last touch if (gesture_in_progress && e.touches.length === 0) { - + var time = new Date().getTime(); // Get corresponding mouse button @@ -887,7 +894,7 @@ Guacamole.Mouse.Touchpad = function Touchpad(element) { // Delay mouse up - mouse up should be canceled if // touchstart within timeout. click_release_timeout = window.setTimeout(function() { - + // Fire button up event guac_touchpad.release(button, e); @@ -1141,7 +1148,7 @@ Guacamole.Mouse.Touchscreen = function Touchscreen(element) { /** * Begins a new gesture at the location of the first touch in the given * touch event. - * + * * @private * @param {!TouchEvent} e * The touch event beginning this new gesture. @@ -1156,7 +1163,7 @@ Guacamole.Mouse.Touchscreen = function Touchscreen(element) { /** * End the current gesture entirely. Wait for all touches to be done before * resuming gesture detection. - * + * * @private */ function end_gesture() { diff --git a/src/Parser.js b/src/Parser.js index 8cd93f2..aa3dbe5 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -208,7 +208,7 @@ Guacamole.Parser = function Parser() { // Immediately truncate buffer if its contents have been // completely parsed, so that the next call to receive() // need not append to the buffer unnecessarily - if (elementEnd + 1 === buffer.length) { + if (!isBuffer && elementEnd + 1 === buffer.length) { elementEnd = -1; buffer = ''; } diff --git a/src/SessionRecording.js b/src/SessionRecording.js index 619f78f..95f881a 100644 --- a/src/SessionRecording.js +++ b/src/SessionRecording.js @@ -394,17 +394,7 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) * the recording. */ function initializeKeyInterpreter(startTimestamp) { - - keyEventInterpreter = new Guacamole.KeyEventInterpreter(null, startTimestamp); - - // Pass through any received batches to the recording ontext handler - keyEventInterpreter.onbatch = function onbatch(batch) { - - // Pass the batch through if a handler is set - if (recording.ontext) - recording.ontext(batch); - - }; + keyEventInterpreter = new Guacamole.KeyEventInterpreter(startTimestamp); } /** @@ -527,11 +517,10 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) instructionBuffer = ''; } - // If there's any typed text that's yet to be sent to the ontext - // handler, send it now - var batch = keyEventInterpreter.getCurrentBatch(); - if (batch && recording.ontext) - recording.ontext(batch); + // Now that the recording is fully processed, and all key events + // have been extracted, call the onkeyevents handler if defined + if (recording.onkeyevents) + recording.onkeyevents(keyEventInterpreter.getEvents()); // Consider recording loaded if tunnel has closed without errors if (!errorEncountered) @@ -919,14 +908,15 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) this.onpause = null; /** - * Fired whenever a new batch of typed text extracted from key events - * is available. + * Fired with all extracted key events when the recording is fully + * processed. The callback will be invoked with an empty list + * if no key events were extracted. * * @event - * @param {!Guacamole.KeyEventInterpreter.KeyEventBatch} batch - * The batch of extracted text. + * @param {!Guacamole.KeyEventInterpreter.KeyEvent[]} batch + * The extracted key events. */ - this.ontext = null; + this.onkeyevents = null; /** * Fired whenever the playback position within the recording changes.