(function() { var chars = { comma: ',', dot: '.', minus: '-', quote: '\'', semicolon: ';', slash: '/', space: ' ' }, keyboardCallbacks = {}, keyboardEventRegExp = /^key(\.[\w\d.]+)?$/, keys = '', keysEventRegExp = new RegExp( '^[\\w\\d](\\.numpad)?$|^(' + Object.keys(chars).join('|') + ')$' ), resetTimeout, triggerTimeout; function bind(options) { var args = Ox.slice(arguments, 1), callbacks = options.callbacks, that = this, oxid = that.oxid || 0; Ox.forEach( Ox.isFunction(args[0]) ? {'*': args[0]} : Ox.makeObject(args), function(originalCallback, event) { event = event.replace(/^key_/, 'key.'); callbacks[event] = (callbacks[event] || []).concat( options.once ? function callback() { unbind.call( that, {callbacks: callbacks}, event, callback ); return originalCallback.apply(null, arguments); } : originalCallback ); if (isKeyboardEvent(event)) { keyboardCallbacks[oxid] = ( keyboardCallbacks[oxid] || [] ).concat(event); } } ); return this; } function isKeyboardEvent(event) { return keyboardEventRegExp.test(event); } function isKeysEventKey(key) { return keysEventRegExp.test(key); } function onMessage(e) { var element, message = {}; try { message = Ox.extend({data: {}}, JSON.parse(e.data)); } catch (e) {} if (message.event == 'init') { if (message.data.oxid) { // The inner window receives the oxid of the outer iframe element Ox.oxid = message.data.oxid; Ox.parent.postMessage('init', {}) } else if (message.target) { // The outer window receives init from iframe Ox.elements[message.target].triggerEvent('init'); } } else { (message.target ? Ox.elements[message.target] : Ox.parent) .triggerMessage(message.event, message.data); } } function onKeydown(e) { var $element = Ox.Focus.focusedElement(), isInput = Ox.Focus.focusedElementIsInput(), keyName = Ox.KEYS[e.keyCode], keyBasename = keyName.split('.')[0], key = Object.keys(Ox.MODIFIER_KEYS).filter(function(key) { return e[key] && Ox.MODIFIER_KEYS[key] != keyBasename; }).map(function(key) { return Ox.MODIFIER_KEYS[key]; }).concat(keyName).join('_'), event = 'key.' + key, triggerEvent = function() { if ($element) { $element.triggerEvent.apply($element, arguments); } else if (!isInput) { Ox.Event.trigger.apply( Ox.$body, [{}].concat(Ox.slice(arguments)) ); } }; triggerEvent(event, e); if (!isInput) { if (isKeysEventKey(key)) { // don't register leading spaces or trailing double spaces if (keyName != 'space' || ( keys != '' && !Ox.endsWith(keys, ' ') )) { keys += chars[keyName] || keyBasename; // clear the trigger timeout only if the key registered clearTimeout(triggerTimeout); triggerTimeout = setTimeout(function() { triggerEvent('keys', Ox.extend(e, {keys: keys})); }, 250); } } // clear the reset timeout even if the key didn't register clearTimeout(resetTimeout); resetTimeout = setTimeout(function() { keys = ''; }, 1000); if (Ox.contains(keyboardCallbacks[0], event) || ( $element && keyboardCallbacks[$element.oxid] && Ox.contains(keyboardCallbacks[$element.oxid], event) )) { // if there is a global handler for this keyboard event, or a // handler on the focused element, then prevent default e.preventDefault(); } } } function trigger(options) { var args = Ox.slice(arguments, 1), callbacks = options.callbacks, that = this; Ox.forEach(Ox.makeObject(args), function(data, originalEvent) { var events = originalEvent.split('.'); ['*'].concat(events.map(function(event, index) { return events.slice(0, index + 1).join('.'); })).forEach(function(event) { (callbacks[0][event] || []) .concat(callbacks[1][event] || []) .forEach(function(callback) { callback.call(that, data, originalEvent, that); }); }); }); return this; } function unbind(options) { var args = Ox.slice(arguments, 1), callbacks = options.callbacks, oxid = this.oxid || 0; if (args.length == 0) { // unbind all handlers for all events callbacks = []; } else { Ox.forEach( Ox.isFunction(args[0]) ? {'*': args[0]} : Ox.makeObject(args), function(callback, event) { if (!callback) { // unbind all handlers for this event delete callbacks[event]; } else if (callbacks[event]) { // unbind this handler for this event callbacks[event] = callbacks[event].filter( function(eventCallback) { return eventCallback !== callback; } ); if (callbacks[event].length == 0) { delete callbacks[event]; } } if (isKeyboardEvent(event)) { var index = keyboardCallbacks[oxid].indexOf(event); keyboardCallbacks[oxid].splice( keyboardCallbacks[oxid].indexOf(event), 1 ) if (keyboardCallbacks[oxid].length == 0) { delete keyboardCallbacks[oxid]; } } } ); } return this; } /*@ Ox.parent Proxy to be used by iframes for messaging with outer window @*/ Ox.parent = Ox.$parent = (function() { var self = {messageCallbacks: {}}, that = {oxid: Ox.uid()}; /*@ bindMessage Adds one or more message handlers @*/ that.bindMessage = function() { return Ox.Message.bind.apply( this, [self].concat(Ox.slice(arguments)) ); }; /*@ bindMessageOnce Adds one or more message handlers that run only once @*/ that.bindMessageOnce = function() { return Ox.Message.bindOnce.apply( this, [self].concat(Ox.slice(arguments)) ); }; /*@ postMessage Sends one or more messages @*/ that.postMessage = function() { if (window !== window.top) { // There actually is an outer window if (!Ox.oxid) { // Inner window has not received init message yet setTimeout(function() { that.postMessage.apply(that, arguments); }, 250); } else { return Ox.Message.post.apply(this, arguments); } } }; /*@ triggerMessage Triggers all handlers for one or more messages @*/ that.triggerMessage = function() { return Ox.Message.trigger.apply( this, [self].concat(Ox.slice(arguments)) ); }; /*@ unbindMessage Removes one or more message handlers @*/ that.unbindMessage = function() { return Ox.Message.unbind.apply( this, [self].concat(Ox.slice(arguments)) ); }; return that; }()); /*@ Ox.Event Event controller @*/ Ox.Event = (function() { var callbacks = {}, that = {}; /*@ bind Adds one or more event handlers ([self, ]callback) -> This method's `this` binding Adds a catch-all handler ([self, ]event, callback) -> This method's `this` binding Adds a handler for a single event ([self, ]{event: callback, ...}) -> This method's `this` binding Adds handlers for multiple events self Object with `eventCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is global and is not bound to a specific `Ox.Element` event Event name callback Callback function data Event data (key/value pairs) event Event name element Element object (this method's `this` binding) @*/ that.bind = function() { var isElement = this !== that; return bind.apply(this, [{ callbacks: isElement ? arguments[0].eventCallbacks : callbacks }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; /*@ bindOnce Adds one or more event handlers that run only once ([self, ]callback) -> This method's `this` binding Adds a catch-all handler ([self, ]event, callback) -> This method's `this` binding Adds a handler for a single event ([self, ]{event: callback, ...}) -> This method's `this` binding Adds handlers for multiple events self Object with `eventCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is global and is not bound to a specific `Ox.Element` event Event name callback Callback function data Event data (key/value pairs) event Event name element Element object (this method's `this` binding) @*/ that.bindOnce = function() { var isElement = this !== that; return bind.apply(this, [{ callbacks: isElement ? arguments[0].eventCallbacks : callbacks, once: true }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; /*@ trigger Triggers all event handlers for one or more events ([self, ]event[, data]) -> This method's `this` binding Triggers one event, with optional event data ([self, ]{event: data, ...}) -> This method's `this` binding Triggers multiple events self Object with `eventCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is global and is not bound to a specific `Ox.Element` event Event name data Event data (key/value pairs) @*/ that.trigger = function() { var isElement = this !== that; return trigger.apply(this, [{ callbacks: [ callbacks, isElement ? arguments[0].eventCallbacks || {} : {} ] }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; /*@ unbind Removes one or more event handlers ([self]) -> This method's `this` binding Unbinds all handlers ([self, ]callback) -> This method's `this` binding Unbinds a catch-all handler ([self, ]event, callback) -> This method's `this` binding Unbinds a handler for a single event ([self, ]{event: callback, ...}) -> This method's `this` binding Unbinds handlers for multiple events self Object with `eventCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is global and is not bound to a specific `Ox.Element` event Event name callback Event handler @*/ that.unbind = function() { var isElement = this !== that; return unbind.apply(this, [{ callbacks: isElement ? arguments[0].eventCallbacks : callbacks }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; return that; }()); /*@ Ox.Message Message controller @*/ Ox.Message = (function() { var callbacks = {}, that = {}; /*@ bind Adds one or more message handlers ([self, ]callback) -> This method's `this` binding Adds a catch-all handler ([self, ]message, callback) -> This method's `this` binding Adds a handler for a single message ([self, ]{message: callback, ...}) -> This method's `this` binding Adds handlers for multiple messages self Object with `messageCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is bound to the outer window (via `Ox.parent`) message Message name callback Callback function data Message data (key/value pairs) message Message name element Element object (this method's `this` binding) @*/ that.bind = function() { var isElement = this !== that; return bind.apply(this, [{ callbacks: isElement ? arguments[0].messageCallbacks : callbacks }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; /*@ bindOnce Adds one or more message handlers that run only once ([self, ]callback) -> This method's `this` binding Adds a catch-all handler ([self, ]message, callback) -> This method's `this` binding Adds a handler for a single message ([self, ]{message: callback, ...}) -> This method's `this` binding Adds handlers for multiple messages self Object with `messageCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is bound to the outer window (via `Ox.parent`) message Message name callback Callback function data Message data (key/value pairs) message Message name element Element object (this method's `this` binding) @*/ that.bindOnce = function() { var isElement = this !== that; return bind.apply(this, [{ callbacks: isElement ? arguments[0].messageCallbacks : callbacks, once: true }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; /*@ post Post a message into our out of an iframe (message[, data]) -> This method's `this` binding Posts one message, with optional message data ({message: data, ...}) -> This method's `this` binding Posts multiple messages message Message name data Message data (key/value pairs) @*/ that.post = function() { var isParent = this == Ox.parent, target = isParent ? window.parent : this[0].contentWindow; Ox.forEach( Ox.makeObject(Ox.slice(arguments)), function(data, event) { target.postMessage(JSON.stringify({ data: data, event: event, target: isParent ? Ox.oxid : null }), '*'); } ); return this; }; /*@ trigger Triggers all message handlers for one or more messages ([self, ]message[, data]) -> This method's `this` binding Triggers one message, with optional message data ([self, ]{message: data, ...}) -> This method's `this` binding Triggers multiple messages self Object with `eventCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is global and is not bound to a specific `Ox.Element` message Message name data Message data (key/value pairs) @*/ that.trigger = function() { var isElement = this !== that; return trigger.apply(this, [{ callbacks: [ callbacks, isElement ? arguments[0].messageCallbacks || {} : {} ] }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; /*@ unbind Removes one or more message handlers ([self, ]callback) -> This method's `this` binding Removes a catch-all handler ([self, ]message, callback) -> This method's `this` binding Removes a handler for a single message ([self, ]{message: callback, ...}) -> This method's `this` binding Removes handlers for multiple messages self Object with `messageCallbacks` (`Ox.Element`'s `self`) If `self` is missing and this method is not rebound, then the handler is bound to the outer window (via `Ox.parent`) message Message name callback Message handler @*/ that.unbind = function() { var isElement = this !== that; return unbind.apply(this, [{ callbacks: isElement ? arguments[0].messageCallbacks : callbacks }].concat(Ox.slice(arguments, isElement ? 1 : 0))); }; return that; }()); document.addEventListener('keydown', onKeydown); window.addEventListener('message', onMessage); }());