diff --git a/source/Ox.UI/js/Core/Event.js b/source/Ox.UI/js/Core/Event.js index 9b4b13ef..3f89225a 100644 --- a/source/Ox.UI/js/Core/Event.js +++ b/source/Ox.UI/js/Core/Event.js @@ -1,197 +1,310 @@ -'use strict'; +(function() { -/*@ -Ox.Event Event controller -@*/ + var chars = { + comma: ',', + dot: '.', + minus: '-', + quote: '\'', + semicolon: ';', + slash: '/', + space: ' ' + }, + hasCallback = {}, + keyboardEventRegExp = /^key(\.[\w\d.]+)?$/, + keys = '', + keysEventRegExp = new RegExp( + '^[\\w\\d](\\.numpad)?$|^(' + Object.keys(chars).join('|') + ')$' + ), + resetTimeout, + triggerTimeout; -Ox.Event = (function() { + function bind(options) { + var args = Ox.slice(arguments, 1), + callbacks = options.callbacks, + that = this; + 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)) { + hasCallback[event] = true; + } + } + ); + return this; + } - var eventHandlers = [], - that = {}; + function isKeyboardEvent(event) { + return keyboardEventRegExp.test(event); + } - function log(data, event, self) { - var element = this, - handlers = self.eventHandlers ? self.eventHandlers[event] : []; - if (!Ox.contains([ - 'mousedown', 'mouserepeat', 'anyclick', 'singleclick', 'doubleclick', 'mousewheel', - 'dragstart', 'drag', 'dragenter', 'dragleave', 'dragpause', 'dragend', - 'draganddropstart', 'draganddrop', 'draganddropenter', 'draganddropleave', 'draganddropend', - 'playing', 'position', 'progress' - ], event)) { - try { - data = JSON.stringify(data) - } catch(e) {} - Ox.print( - 'EVENT', - element.oxid, - '"' + element[0].className.split(' ').filter(function(className) { - return /^Ox/.test(className); - }).map(function(className) { - return className.replace(/^Ox/, ''); - }).join(' ') + '"', - event, - data, - handlers.length, - handlers.map(function(handler) { - return handler.toString().split('\n').shift(); - }) - ); + 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); } } - /*@ - .bind Adds event handler(s) - (callback) -> Ox.Event - Adds a global event handler - (self, callback) -> Ox.Event - Adds a catch-all handler - (self, event, callback) -> Ox.Event - Adds a handler for a single event - (self, {event: callback, ...}) -> Ox.Event - Adds handlers for multiple events - self The element's shared private object - callback Callback function - data Event data - event Event name - element Element - event Event name - Event names can be namespaced, like `'click.foo'` - */ - that.bind = function() { - var args = Ox.slice(arguments), once, self; - if (args.length == 1) { - eventHandlers.push(args[0]) - } else { - self = args.shift(); - once = Ox.isBoolean(Ox.last(args)) ? args.pop() : false; - args = Ox.isFunction(args[0]) ? {'*': args[0]} : Ox.makeObject(args); - if (Ox.len(args) && !self.eventHandlers) { - self.eventHandlers = {}; - } - Ox.forEach(args, function(callback, event) { - self.eventHandlers[event] = ( - self.eventHandlers[event] || [] - ).concat({callback: callback, once: once}); - }); - } - return that; - }; - - /*@ - .bindOnce Adds event handler(s) that run(s) only once - (self, callback) -> Ox.Event - Adds a catch-all handler - (self, event, callback) -> Ox.Event - Adds an event handler for a single event - (self, {event: callback, ...}) -> Ox.Event - Adds event handlers for multiple events - self The element's shared private object - callback Callback function - data Event data - event Event name - element Element - event Event name - Event names can be namespaced, like `'click.foo'` - */ - that.bindOnce = function() { - return that.bind.apply(null, Ox.slice(arguments).concat(true)); - }; - - /*@ - .log Turns event logging on or off - (enabled) -> Ox.Event - enabled Enables (`true`) or disables (`false`) event logging - */ - that.log = function(enabled) { - that[enabled ? 'bind' : 'unbind'](log); - return that; - }; - - /*@ - .trigger Triggers an event - (self, event) -> Ox.Event - Triggers an event - (self, event, data) -> Ox.Event - Triggers an event with data - (self, {event: data, ...}) -> Ox.Event - Triggers multiple events with data - self The element's shared private object - event Event name - data Event data - */ - that.trigger = function(self) { - var args = arguments, element = this; - if (self.eventHandlers) { - Ox.forEach(Ox.makeObject(Ox.slice(args, 1)), function(data, event) { - var triggered = event.split('.'); - eventHandlers.forEach(function(callback) { - callback.call(element, data || {}, event, element); - }); - triggered.map(function(v, i) { - return triggered.slice(0, i + 1).join('.'); - }).concat('*').forEach(function(triggered) { - var handlers = self.eventHandlers[triggered]; - handlers && handlers.forEach(function(handler, i) { - handler.once && handlers.splice(i, 1); - handler.callback.call(element, data || {}, event); - }); - }); - }); - } - return that; - }; - - /*@ - .unbind Removes an event handler - () -> Ox.Event - Removes all global handlers - (callback) -> Ox.Event - Removes a global handler - (self) -> Ox.Event - Removes all handlers - (self, callback) -> Ox.Event - Removes a specific catch-all handler - (self, event) -> Ox.Event - Remove all handlers for a single event - (self, event, callback) -> Ox.Event - Removes a specific handler for a single event - (self, {event: callback, ...}) -> Ox.Event - Removes specific event handlers for multiple events - self The element's shared private object - callback Callback function - event Event name - */ - that.unbind = function() { - var args = Ox.slice(arguments), self; - if (args.length == 0) { - eventHandlers = []; - } else if (Ox.isFunction(args[0])) { - eventHandlers.forEach(function(handler, i) { - handler === args[0] && eventHandlers.splice(i, 1); - }); - } else if ((self = args.shift()).eventHandlers) { - if (args.length == 0) { - delete self.eventHandlers; - } else { - if (Ox.isFunction(args[0])) { - args = {'*': args[0]}; + 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('_'), + triggerEvent = function() { + if ($element) { + $element.triggerEvent.apply($element, arguments); + } else { + Ox.Event.trigger.apply( + Ox.$body, [{}].concat(Ox.slice(arguments)) + ); } - Ox.forEach(Ox.makeObject(args), function(callback, event) { - if (Ox.isUndefined(callback)) { - delete self.eventHandlers[event]; - } else { - self.eventHandlers[event].forEach(function(handler, i) { - if (handler.callback === callback) { - self.eventHandlers[event].splice(i, 1); - } - }); - } - }); + }; + triggerEvent('key.' + key, e); + 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 (hasCallback[key]) { + 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; + 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) && !callbacks[event]) { + delete hasCallback[event]; + } + } + ); + } + return this; + } + + Ox.$parent = Ox.parent = (function() { + + var self = {messageCallbacks: {}}, + that = {oxid: Ox.uid()}; + + that.bindMessage = function() { + return Ox.Message.bind.apply( + this, [self].concat(Ox.slice(arguments)) + ); + }; + + that.bindMessageOnce = function() { + return Ox.Message.bindOnce.apply( + this, [self].concat(Ox.slice(arguments)) + ); + }; + + that.postMessage = function() { + return Ox.Message.post.apply(this, Ox.slice(arguments)); + }; + + that.triggerMessage = function() { + return Ox.Message.trigger.apply( + this, [self].concat(Ox.slice(arguments)) + ); + }; + + that.unbindMessage = function() { + return Ox.Message.unbind.apply( + this, [self].concat(Ox.slice(arguments)) + ); + }; + return that; - }; - return that; + }()); -}()); + Ox.Event = (function() { + + var callbacks = {}, + that = {}; + + that.bind = function() { + var isElement = this !== that; + return bind.apply(this, [{ + callbacks: isElement ? arguments[0].eventCallbacks : callbacks + }].concat(Ox.slice(arguments, isElement ? 1 : 0))); + }; + + 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))); + }; + + that.trigger = function(self) { + return trigger.apply(this, [{ + callbacks: [callbacks, self.eventCallbacks || {}] + }].concat(Ox.slice(arguments, 1))); + }; + + 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 = (function() { + + var callbacks = {}, + that = {}; + + that.bind = function() { + var isElement = this !== that; + return bind.apply(this, [{ + callbacks: isElement ? arguments[0].messageCallbacks + : callbacks + }].concat(Ox.slice(arguments, isElement ? 1 : 0))); + }; + + 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))); + }; + + that.post = function() { + var args = arguments, + isParent = this == Ox.parent, + target = isParent ? window.parent : this[0].contentWindow, + that = this; + if (isParent && !Ox.oxid) { + // posting to parent, but not yet initialized + setTimeout(function() { + that.post.apply(Ox.parent, args); + }, 25); + } + Ox.forEach( + Ox.makeObject(Ox.slice(args)), + function(data, event) { + target.postMessage(JSON.stringify({ + data: data, + event: event, + target: isParent ? Ox.oxid : null + }), '*'); + } + ); + }; + + that.trigger = function(self) { + return trigger.apply(this, [{ + callbacks: [callbacks, self.messageCallbacks] + }].concat(Ox.slice(arguments, 1))); + }; + + 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); + +}()); \ No newline at end of file