'use strict'; /*@ Ox.Element Basic UI element object # Arguments ---------------------------------------------------------------- options Options of the element, or just the `element` option element Tagname or CSS selector tooltip Tooltip title, or a function that returns one (e) -> Tooltip title e Mouse event self Shared private variable # Usage -------------------------------------------------------------------- ([options[, self]]) -> Element object # Events --------------------------------------------------------------- anyclick anyclick Fires on mouseup, but not on any subsequent mouseup within 250 ms (this is useful if one wants to listen for singleclicks, but not doubleclicks, since it will fire immediately, and won't fire again in case of a doubleclick) * <*> Original event properties doubleclick doubleclick Fires on the second mousedown within 250 ms (this is useful if one wants to listen for both singleclicks and doubleclicks, since it will not trigger a singleclick event) * <*> Original event properties drag drag Fires on mousemove after dragstart, stops firing on mouseup clientDX Horizontal drag delta in px clientDY Vertical drag delta in px * <*> Original event properties dragend dragpause Fires on mouseup after dragstart clientDX Horizontal drag delta in px clientDY Vertical drag delta in px * <*> Original event properties dragenter dragenter Fires when entering an element during drag (this fires on the element being dragged -- the target element is the event's target property) clientDX Horizontal drag delta in px clientDY Vertical drag delta in px * <*> Original event properties dragleave dragleave Fires when leaving an element during drag (this fires on the element being dragged -- the target element is the event's target property) clientDX Horizontal drag delta in px clientDY Vertical drag delta in px * <*> Original event properties dragpause dragpause Fires once when the mouse doesn't move for 250 ms during drag (this is useful in order to execute operations that are too expensive to be attached to the drag event) clientDX Horizontal drag delta in px clientDY Vertical drag delta in px * <*> Original event properties mousedown mousedown Fires on mousedown (this is useful if one wants to listen for singleclicks, but not doubleclicks or drag events, and wants the event to fire as early as possible) * <*> Original event properties dragstart dragstart Fires when the mouse is down for 250 ms * <*> Original event properties mouserepeat mouserepeat Fires every 50 ms after the mouse was down for 250 ms, stops firing on mouseleave or mouseup (this fires like a key that is being pressed and held, and is useful for buttons like scrollbars arrows that need to react to both clicking and holding) singleclick singleclick Fires 250 ms after mouseup, if there was no subsequent mousedown (this is useful if one wants to listen for both singleclicks and doubleclicks, since it will not fire for doubleclicks) * <*> Original event properties */ Ox.Element = function(options, self) { // create private object self = self || {}; // create defaults and options objects self.defaults = {}; self.options = options || {}; // allow for Ox.TestElement('') // or Ox.TestElement('cssSelector') if (Ox.isString(self.options)) { self.options = { element: self.options }; } // create event handler // (this can be passed as part of self) if (!self.$eventHandler) { self.$eventHandler = $('
'); } // array of callbacks bound to any event self.eventCallbacks = []; // stack of callbacks bound to option updates self.updateCallbacks = []; // create public object var that = new Ox.JQueryElement($(self.options.element || '
')) .addClass('OxElement') .mousedown(mousedown); setTooltip(); function bind(event, callback, once) { self.$eventHandler[ once ? 'one' : 'on' ]('ox_' + event, function(event, data) { call(callback, data, event); }); } function call(callback, data, event) { event.ox_id = that.oxid; event.ox_type = event.type.replace(/^ox_/, ''); callback.call(that, data || {}, event); } function mousedown(e) { /* better mouse events mousedown: trigger mousedown within 250 msec: mouseup: trigger anyclick mouseup + mousedown: trigger doubleclick after 250 msec: mouseup + no mousedown within 250 msec: trigger singleclick no mouseup within 250 msec: trigger mouserepeat every 50 msec trigger dragstart mousemove: trigger drag no mousemove for 250 msec: trigger dragpause mouseup: trigger dragend "anyclick" is not called "click" since this would collide with the click events of some widgets */ var clientX, clientY, dragTimeout = 0, mouseInterval = 0; if (!self._mouseTimeout) { // first mousedown that.triggerEvent('mousedown', e); self._drag = false; self._mouseup = false; self._mouseTimeout = setTimeout(function() { // 250 ms later, no subsequent click self._mouseTimeout = 0; if (self._mouseup) { // mouse went up, trigger singleclick that.triggerEvent('singleclick', e); } else { // mouse is still down, trigger mouserepeat // every 50 ms until mouseleave or mouseup mouserepeat(); mouseInterval = setInterval(mouserepeat, 50); that.one('mouseleave', function() { clearInterval(mouseInterval); }); // trigger dragstart, set up drag events that.triggerEvent('dragstart', e); $('.OxElement').on({ mouseenter: dragenter, mouseleave: dragleave }); clientX = e.clientX; clientY = e.clientY; Ox.UI.$window .off('mouseup', mouseup) .mousemove(mousemove) .one('mouseup', function(e) { // stop checking for mouserepeat clearInterval(mouseInterval); // stop checking for dragpause clearTimeout(dragTimeout); // stop checking for drag Ox.UI.$window.off('mousemove', mousemove); // stop checking for dragenter and dragleave $('.OxElement').off({ mouseenter: dragenter, mouseleave: dragleave }); // trigger dragend that.triggerEvent('dragend', extend(e)); }); self._drag = true; } }, 250); } else { // second mousedown within 250 ms, trigger doubleclick clearTimeout(self._mouseTimeout); self._mouseTimeout = 0; that.triggerEvent('doubleclick', e); } Ox.UI.$window.one('mouseup', mouseup); function dragenter(e) { that.triggerEvent('dragenter', extend(e)); } function dragleave(e) { that.triggerEvent('dragleave', extend(e)); } function extend(e) { return Ox.extend({ clientDX: e.clientX - clientX, clientDY: e.clientY - clientY }, e); } function mousemove(e) { e = extend(e); clearTimeout(dragTimeout); dragTimeout = setTimeout(function() { // mouse did not move for 250 ms, trigger dragpause that.triggerEvent('dragpause', e); }, 250); that.triggerEvent('drag', e); } function mouserepeat(e) { that.triggerEvent('mouserepeat', e); } function mouseup(e) { if (!self._mouseup && !self._drag) { // mouse went up for the first time, trigger anyclick that.triggerEvent('anyclick', e); self._mouseup = true; } } } function mouseenter(e) { that.$tooltip.show(e); } function mouseleave(e) { that.$tooltip.hide(); } function mousemove(e) { that.$tooltip.options({ title: self.options.tooltip(e) }).show(e); } // FIXME: in other widgets, use this, // rather than some self.$tooltip that // will not get garbage collected function setTooltip() { if (self.options.tooltip) { if (Ox.isString(self.options.tooltip)) { that.$tooltip = Ox.Tooltip({ title: self.options.tooltip }); that.on({ mouseenter: mouseenter }).off({ mousemove: mousemove }); } else { that.$tooltip = Ox.Tooltip({ animate: false }); that.on({ mousemove: mousemove }).off({ mouseenter: mouseenter }); } that.on({ mouseleave: mouseleave }); } else { if (that.$tooltip) { that.$tooltip.remove(); that.off({ mouseenter: mouseenter, mousemove: mousemove, mouseleave: mouseleave }); } } } function update(key, value) { // update is called whenever an option is modified or added Ox.loop(self.updateCallbacks.length - 1, -1, -1, function(i) { self.updateCallbacks[i](key, value) === false && Ox.Break(); }); } /*@ bindEvent Binds a function to an event (callback) -> This element (event, callback) -> This element ({event: callback, ...}) -> This element callback Callback function data event data (key/value pairs) event Event name Event names can be namespaced, like `'click.foo'` @*/ that.bindEvent = function() { if (Ox.typeOf(arguments[0]) == 'function') { self.eventCallbacks.push(arguments[0]); } else { Ox.forEach(Ox.makeObject(arguments), function(callback, event) { bind(event, callback); }); } return that; }; /*@ bindEventOnce Binds a function to an event, once (event, callback) -> This element object ({event: callback, ...}) -> This element object callback Callback function data event data (key/value pairs) event Event name Event names can be namespaced, like `'click.foo'` @*/ that.bindEventOnce = function() { Ox.forEach(Ox.makeObject(arguments), function(callback, event) { bind(event, callback, true); }); return that; }; /*@ bindKeyboard bind keyboard () -> object @*/ that.bindKeyboard = function() { Ox.Keyboard.bind(that.oxid); return that; }; /*@ defaults Gets or sets the default options for an element object ({key: value, ...}) -> This element object key The name of the default option value <*> The value of the default option @*/ that.defaults = function() { var ret; if (arguments.length == 0) { ret = self.defaults; } else if (Ox.isString(arguments[0])) { ret = self.defaults[arguments[0]]; } else { self.defaults = arguments[0]; self.options = Ox.clone(self.defaults); ret = that; } return ret; }; /*@ gainFocus Makes an element object gain focus () -> This element object @*/ that.gainFocus = function() { Ox.Focus.focus(that.oxid); return that; }; /*@ hasFocus Returns true if an element object has focus () -> True if the element has focus @*/ that.hasFocus = function() { return Ox.Focus.focused() == that.oxid; }; /*@ loseFocus Makes an element object lose focus () -> This element object @*/ that.loseFocus = function() { Ox.Focus.blur(that.oxid); return that; }; /*@ options Gets or sets the options of an element object # Usage () -> All options (key) -> <*> The value of option[key] (key, value) -> This element Sets options[key] to value and calls update(key, value) if the key/value pair was added or modified ({key: value, ...}) -> this element Sets multiple options and calls update(key, value) for every key/value pair that was added or modified # Arguments key The name of the option value <*> The value of the option @*/ that.options = function() { return Ox.getset(self.options, arguments, update, that); }; /*@ removeElement Removes an element object and its event handler () -> This element @*/ that.remove = function(remove) { remove !== false && that.find('.OxElement').each(function() { var oxid = $(this).data('oxid'), element = Ox.UI.elements[oxid]; element && element.remove(false); }); Ox.Focus.remove(that.oxid); Ox.Keyboard.unbind(that.oxid); delete self.$eventHandler; delete Ox.UI.elements[that.oxid]; that.$tooltip && that.$tooltip.remove(); remove !== false && that.$element.remove(); return that; }; /*@ setElement set $element ($element) -> This element @*/ that.setElement = function($element) { $element.addClass('OxElement').data({oxid: that.oxid}); that.$element.replaceWith($element); that.$element = $element; that[0] = that.$element[0]; return that; }; /*@ toggleOption Toggle boolean option(s) (key[, key[, ...]]) -> This element @*/ that.toggleOption = function() { var options = {}; Ox.toArray(arguments).forEach(function(key) { options[key] == !self.options[key]; }); that.options(options); return that; }; /*@ triggerEvent Triggers an event (event) -> This element object (event, data) -> This element object ({event: data, ...}) -> This element object event Event name data Event data (key/value pairs) @*/ that.triggerEvent = function() { Ox.forEach(Ox.makeObject(arguments), function(data, event) { var type = 'ox_' + event; if ([ 'mousedown', 'mouserepeat', 'anyclick', 'singleclick', 'doubleclick', 'dragstart', 'drag', 'dragenter', 'dragleave', 'dragpause', 'dragend', 'draganddropstart', 'draganddrop', 'draganddropenter', 'draganddropleave', 'draganddropend', 'playing', 'position', 'progress', 'request' ].indexOf(event) == -1) { if (!/^pandora_/.test(event)) { Ox.Log('EVENT', that.oxid, self.options.id, 'trigger', event, data); } } // it is necessary to check if self.$eventHandler exists, // since, for example, when removing the element on click, // singleclick will fire after the removal of the event handler self.$eventHandler && self.$eventHandler.trigger(type, data); self.eventCallbacks.forEach(function(callback) { call(callback, data, {type: type}); }); }); return that; }; /*@ unbindEvent Unbinds all callbacks from an event To unbind a specific handler, use namespaced events, like `bindEvent('click.foo', callback)`, and then `unbindEvent('click.foo')`. () -> This element Unbinds all callbacks from all events (callback) -> This element Unbinds one callback from all events (event) -> This element Unbinds all callbacks from one event (event, callback) -> This element Unbinds one callback from one event ({event: callback}, ...) -> This element Unbinds multiple callbacks from multiple events event Event name @*/ that.unbindEvent = function() { var callback = arguments[0]; if (arguments.length == 0) { self.eventCallbacks = []; self.$eventHandler.off(); } else if (Ox.typeOf(callback) == 'function') { self.eventCallbacks = self.eventCallbacks.filter(function(fn) { return fn !== callback; }); } else { Ox.forEach(Ox.makeObject(arguments), function(callback, event) { self.$eventHandler.off('ox_' + event, callback); }); } return that; }; /*@ unbindKeyboard unbind keyboard () -> object @*/ that.unbindKeyboard = function() { Ox.Keyboard.unbind(that.oxid); return that; }; /*@ update Adds one or more handlers for options updates (callback) -> that (key, callback) -> that ({key: callback, ...}) -> that @*/ that.update = function() { var callbacks; if (Ox.isFunction(arguments[0])) { self.updateCallbacks.push(arguments[0]); } else { callbacks = Ox.makeObject(arguments); self.updateCallbacks.push(function(key) { if (callbacks[key]) { return callbacks[key](); } }); } return that; }; /*@ value Shortcut to get or set self.options.value @*/ that.value = function() { return that.options( arguments.length == 0 ? 'value' : {value: arguments[0]} ); }; that.update({ tooltip: setTooltip }); return that; };