diff --git a/guacamole-common-js/pom.xml b/guacamole-common-js/pom.xml
index e38ea1c39..9025e9a95 100644
--- a/guacamole-common-js/pom.xml
+++ b/guacamole-common-js/pom.xml
@@ -135,7 +135,29 @@
+
+
+
+ com.github.searls
+ jasmine-maven-plugin
+ 2.2
+
+
+
+ test
+
+
+
+
+
+ 2.1.1
+
+
+ **/*.min.js
+
+ ${project.build.directory}/${project.build.finalName}
+
diff --git a/guacamole-common-js/src/main/webapp/modules/Event.js b/guacamole-common-js/src/main/webapp/modules/Event.js
new file mode 100644
index 000000000..d83330553
--- /dev/null
+++ b/guacamole-common-js/src/main/webapp/modules/Event.js
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var Guacamole = Guacamole || {};
+
+/**
+ * An arbitrary event, emitted by a {@link Guacamole.Event.Target}. This object
+ * should normally serve as the base class for a different object that is more
+ * specific to the event type.
+ *
+ * @constructor
+ * @param {String} type
+ * The unique name of this event type.
+ */
+Guacamole.Event = function Event(type) {
+
+ /**
+ * The unique name of this event type.
+ *
+ * @type {String}
+ */
+ this.type = type;
+
+ /**
+ * An arbitrary timestamp in milliseconds, indicating this event's
+ * position in time relative to other events.
+ *
+ * @type {Number}
+ */
+ this.timestamp = new Date().getTime();
+
+ /**
+ * Returns the number of milliseconds elapsed since this event was created.
+ *
+ * @return {Number}
+ * The number of milliseconds elapsed since this event was created.
+ */
+ this.getAge = function getAge() {
+ return new Date().getTime() - this.timestamp;
+ };
+
+ /**
+ * Requests that the legacy event handler associated with this event be
+ * invoked on the given event target. This function will be invoked
+ * automatically by implementations of {@link Guacamole.Event.Target}
+ * whenever {@link Guacamole.Event.Target#emit emit()} is invoked.
+ *
+ * Older versions of Guacamole relied on single event handlers with the
+ * prefix "on", such as "onmousedown" or "onkeyup". If a Guacamole.Event
+ * implementation is replacing the event previously represented by one of
+ * these handlers, this function gives the implementation the opportunity
+ * to provide backward compatibility with the old handler.
+ *
+ * Unless overridden, this function does nothing.
+ *
+ * @param {Guacamole.Event.Target} eventTarget
+ * The {@link Guacamole.Event.Target} that emitted this event.
+ */
+ this.invokeLegacyHandler = function invokeLegacyHandler(eventTarget) {
+ // Do nothing
+ };
+
+};
+
+/**
+ * A {@link Guacamole.Event} that relates to one or more DOM events. Continued
+ * propagation and default behavior of the related DOM events may be prevented
+ * with {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} and
+ * {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()}
+ * respectively.
+ *
+ * @constructor
+ * @augments Guacamole.Event
+ *
+ * @param {String} type
+ * The unique name of this event type.
+ *
+ * @param {Event[]} events
+ * The DOM events that are related to this event. Future calls to
+ * {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()} and
+ * {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} will
+ * affect these events.
+ */
+Guacamole.Event.DOMEvent = function DOMEvent(type, events) {
+
+ Guacamole.Event.call(this, type);
+
+ /**
+ * Requests that the default behavior of related DOM events be prevented.
+ * Whether this request will be honored by the browser depends on the
+ * nature of those events and the timing of the request.
+ */
+ this.preventDefault = function preventDefault() {
+ events.forEach(function applyPreventDefault(event) {
+ if (event.preventDefault) event.preventDefault();
+ event.returnValue = false;
+ });
+ };
+
+ /**
+ * Stops further propagation of related events through the DOM. Only events
+ * that are directly related to this event will be stopped.
+ */
+ this.stopPropagation = function stopPropagation() {
+ events.forEach(function applyStopPropagation(event) {
+ event.stopPropagation();
+ });
+ };
+
+};
+
+/**
+ * An object which can dispatch {@link Guacamole.Event} objects. Listeners
+ * registered with {@link Guacamole.Event.Target#on on()} will automatically
+ * be invoked based on the type of {@link Guacamole.Event} passed to
+ * {@link Guacamole.Event.Target#dispatch dispatch()}. It is normally
+ * subclasses of Guacamole.Event.Target that will dispatch events, and usages
+ * of those subclasses that will catch dispatched events with on().
+ *
+ * @constructor
+ */
+Guacamole.Event.Target = function Target() {
+
+ /**
+ * A callback function which handles an event dispatched by an event
+ * target.
+ *
+ * @callback Guacamole.Event.Target~listener
+ * @param {Guacamole.Event} event
+ * The event that was dispatched.
+ *
+ * @param {Guacamole.Event.Target} target
+ * The object that dispatched the event.
+ */
+
+ /**
+ * All listeners (callback functions) registered for each event type passed
+ * to {@link Guacamole.Event.Targer#on on()}.
+ *
+ * @private
+ * @type {Object.}
+ */
+ var listeners = {};
+
+ /**
+ * Registers a listener for events having the given type, as dictated by
+ * the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
+ * provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
+ *
+ * @param {String} type
+ * The unique name of this event type.
+ *
+ * @param {Guacamole.Event.Target~listener} listener
+ * The function to invoke when an event having the given type is
+ * dispatched. The {@link Guacamole.Event} object provided to
+ * {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
+ * this function, along with the dispatching Guacamole.Event.Target.
+ */
+ this.on = function on(type, listener) {
+
+ var relevantListeners = listeners[type];
+ if (!relevantListeners)
+ listeners[type] = relevantListeners = [];
+
+ relevantListeners.push(listener);
+
+ };
+
+ /**
+ * Dispatches the given event, invoking all event handlers registered with
+ * this Guacamole.Event.Target for that event's
+ * {@link Guacamole.Event#type type}.
+ *
+ * @param {Guacamole.Event} event
+ * The event to dispatch.
+ */
+ this.dispatch = function dispatch(event) {
+
+ // Invoke any relevant legacy handler for the event
+ event.invokeLegacyHandler(this);
+
+ // Invoke all registered listeners
+ var relevantListeners = listeners[event.type];
+ if (relevantListeners) {
+ for (var i = 0; i < relevantListeners.length; i++) {
+ relevantListeners[i](event, this);
+ }
+ }
+
+ };
+
+ /**
+ * Unregisters a listener that was previously registered with
+ * {@link Guacamole.Event.Target#on on()}. If no such listener was
+ * registered, this function has no effect. If multiple copies of the same
+ * listener were registered, the first listener still registered will be
+ * removerd.
+ *
+ * @param {String} type
+ * The unique name of the event type handled by the listener being
+ * removed.
+ *
+ * @param {Guacamole.Event.Target~listener} listener
+ * The listener function previously provided to
+ * {@link Guacamole.Event.Target#on on()}.
+ *
+ * @returns {Boolean}
+ * true if the specified listener was removed, false otherwise.
+ */
+ this.off = function off(type, listener) {
+
+ var relevantListeners = listeners[type];
+ if (!relevantListeners)
+ return false;
+
+ for (var i = 0; i < relevantListeners.length; i++) {
+ if (relevantListeners[i] === listener) {
+ relevantListeners.splice(i, 1);
+ return true;
+ }
+ }
+
+ return false;
+
+ };
+
+};
diff --git a/guacamole-common-js/src/test/javascript/EventSpec.js b/guacamole-common-js/src/test/javascript/EventSpec.js
new file mode 100644
index 000000000..9f94d6fa1
--- /dev/null
+++ b/guacamole-common-js/src/test/javascript/EventSpec.js
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* global Guacamole, jasmine, expect */
+
+describe("Guacamole.Event", function EventSpec() {
+
+ /**
+ * Test subclass of {@link Guacamole.Event} which provides a single
+ * "value" property supports an "ontest" legacy event handler.
+ *
+ * @constructor
+ * @augments Guacamole.Event
+ */
+ var TestEvent = function TestEvent(value) {
+
+ Guacamole.Event.apply(this, [ 'test' ]);
+
+ /**
+ * An arbitrary value to expose to the handler of this event.
+ *
+ * @type {Object}
+ */
+ this.value = value;
+
+ /**
+ * @inheritdoc
+ */
+ this.invokeLegacyHandler = function invokeLegacyHandler(target) {
+ if (target.ontest)
+ target.ontest(value);
+ };
+
+ };
+
+ /**
+ * Event target instance which will receive each fired {@link TestEvent}.
+ *
+ * @type {Guacamole.Event.Target}
+ */
+ var eventTarget;
+
+ beforeEach(function() {
+ eventTarget = new Guacamole.Event.Target();
+ });
+
+ describe("when an event is dispatched", function(){
+
+ it("should invoke the legacy handler for matching events", function() {
+
+ eventTarget.ontest = jasmine.createSpy('ontest');
+ eventTarget.dispatch(new TestEvent('event1'));
+ expect(eventTarget.ontest).toHaveBeenCalledWith('event1');
+
+ });
+
+ it("should invoke all listeners for matching events", function() {
+
+ var listener1 = jasmine.createSpy('listener1');
+ var listener2 = jasmine.createSpy('listener2');
+
+ eventTarget.on('test', listener1);
+ eventTarget.on('test', listener2);
+
+ eventTarget.dispatch(new TestEvent('event2'));
+
+ expect(listener1).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event2' }), eventTarget);
+ expect(listener2).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event2' }), eventTarget);
+
+ });
+
+ it("should not invoke any listeners for non-matching events", function() {
+
+ var listener1 = jasmine.createSpy('listener1');
+ var listener2 = jasmine.createSpy('listener2');
+
+ eventTarget.on('test2', listener1);
+ eventTarget.on('test2', listener2);
+
+ eventTarget.dispatch(new TestEvent('event3'));
+
+ expect(listener1).not.toHaveBeenCalled();
+ expect(listener2).not.toHaveBeenCalled();
+
+ });
+
+ it("should not invoke any listeners that have been removed", function() {
+
+ var listener1 = jasmine.createSpy('listener1');
+ var listener2 = jasmine.createSpy('listener2');
+
+ eventTarget.on('test', listener1);
+ eventTarget.on('test', listener2);
+ eventTarget.off('test', listener1);
+
+ eventTarget.dispatch(new TestEvent('event4'));
+
+ expect(listener1).not.toHaveBeenCalled();
+ expect(listener2).toHaveBeenCalledWith(jasmine.objectContaining({ type : 'test', value : 'event4' }), eventTarget);
+
+ });
+
+ });
+
+ describe("when listeners are removed", function(){
+
+ it("should return whether a listener is successfully removed", function() {
+
+ var listener1 = jasmine.createSpy('listener1');
+ var listener2 = jasmine.createSpy('listener2');
+
+ eventTarget.on('test', listener1);
+ eventTarget.on('test', listener2);
+
+ expect(eventTarget.off('test', listener1)).toBe(true);
+ expect(eventTarget.off('test', listener1)).toBe(false);
+ expect(eventTarget.off('test', listener2)).toBe(true);
+ expect(eventTarget.off('test', listener2)).toBe(false);
+
+ });
+
+ });
+});