can
Version:
MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.
335 lines (295 loc) • 11.1 kB
JavaScript
// # can/control/control.js
//
// Create organized, memory-leak free, rapidly performing, stateful
// controls with declarative eventing binding. Used when creating UI
// controls with behaviors, bound to elements on the page.
// ## helpers
steal('can/util', 'can/construct', function (can) {
//
// ### bind
// this helper binds to one element and returns a function that unbinds from that element.
var bind = function (el, ev, callback) {
can.bind.call(el, ev, callback);
return function () {
can.unbind.call(el, ev, callback);
};
},
isFunction = can.isFunction,
extend = can.extend,
each = can.each,
slice = [].slice,
paramReplacer = /\{([^\}]+)\}/g,
special = can.getObject("$.event.special", [can]) || {},
// ### delegate
//
// this helper binds to elements based on a selector and returns a
// function that unbinds.
delegate = function (el, selector, ev, callback) {
can.delegate.call(el, selector, ev, callback);
return function () {
can.undelegate.call(el, selector, ev, callback);
};
},
// ### binder
//
// Calls bind or unbind depending if there is a selector.
binder = function (el, ev, callback, selector) {
return selector ?
delegate(el, can.trim(selector), ev, callback) :
bind(el, ev, callback);
},
basicProcessor;
var Control = can.Control = can.Construct(
/**
* @add can.Control
*/
// ## *static functions*
/**
* @static
*/
{
// ## can.Control.setup
//
// This function pre-processes which methods are event listeners and which are methods of
// the control. It has a mechanism to allow controllers to inherit default values from super
// classes, like `can.Construct`, and will cache functions that are action functions (see `_isAction`)
// or functions with an underscored name.
setup: function () {
can.Construct.setup.apply(this, arguments);
if (can.Control) {
var control = this,
funcName;
control.actions = {};
for (funcName in control.prototype) {
if (control._isAction(funcName)) {
control.actions[funcName] = control._action(funcName);
}
}
}
},
// ## can.Control._shifter
//
// Moves `this` to the first argument, wraps it with `jQuery` if it's
// an element.
_shifter: function (context, name) {
var method = typeof name === "string" ? context[name] : name;
if (!isFunction(method)) {
method = context[method];
}
return function () {
context.called = name;
return method.apply(context, [this.nodeName ? can.$(this) : this].concat(slice.call(arguments, 0)));
};
},
// ## can.Control._isAction
//
// Return `true` if `methodName` refers to an action. An action is a `methodName` value that
// is not the constructor, and is either a function or string that refers to a function, or is
// defined in `special`, `processors`. Detects whether `methodName` is also a valid method name.
_isAction: function (methodName) {
var val = this.prototype[methodName],
type = typeof val;
return (methodName !== 'constructor') &&
(type === "function" || (type === "string" && isFunction(this.prototype[val]))) &&
!! (special[methodName] || processors[methodName] || /[^\w]/.test(methodName));
},
// ## can.Control._action
//
// Takes a method name and the options passed to a control and tries to return the data
// necessary to pass to a processor (something that binds things).
//
// For performance reasons, `_action` is called twice:
// * It's called when the Control class is created. for templated method names (e.g., `{window} foo`), it returns null. For non-templated method names it returns the event binding data. That data is added to `this.actions`.
// * It is called wehn a control instance is created, but only for templated actions.
_action: function (methodName, options) {
// If we don't have options (a `control` instance), we'll run this later. If we have
// options, run `can.sub` to replace the action template `{}` with values from the `options`
// or `window`. If a `{}` template resolves to an object, `convertedName` will be an array.
// In that case, the event name we want will be the last item in that array.
paramReplacer.lastIndex = 0;
if (options || !paramReplacer.test(methodName)) {
var convertedName = options ? can.sub(methodName, this._lookup(options)) : methodName;
if (!convertedName) {
//!steal-remove-start
can.dev.log('can/control/control.js: No property found for handling ' + methodName);
//!steal-remove-end
return null;
}
var arr = can.isArray(convertedName),
name = arr ? convertedName[1] : convertedName,
parts = name.split(/\s+/g),
event = parts.pop();
return {
processor: processors[event] || basicProcessor,
parts: [name, parts.join(" "), event],
delegate: arr ? convertedName[0] : undefined
};
}
},
_lookup: function (options) {
return [options, window];
},
// ## can.Control.processors
//
// An object of `{eventName : function}` pairs that Control uses to
// hook up events automatically.
processors: {},
// ## can.Control.defaults
// A object of name-value pairs that act as default values for a control instance
defaults: {}
}, {
// ## *prototype functions*
/**
* @prototype
*/
// ## setup
//
// Setup is where most of the Control's magic happens. It performs several pre-initialization steps:
// - Sets `this.element`
// - Adds the Control's name to the element's className
// - Saves the Control in `$.data`
// - Merges Options
// - Binds event handlers using `delegate`
// The final step is to return pass the element and prepareed options, to be used in `init`.
setup: function (element, options) {
var cls = this.constructor,
pluginname = cls.pluginName || cls._fullName,
arr;
// Retrieve the raw element, then set the plugin name as a class there.
this.element = can.$(element);
if (pluginname && pluginname !== 'can_control') {
this.element.addClass(pluginname);
}
// Set up the 'controls' data on the element. If it does not exist, initialize
// it to an empty array.
arr = can.data(this.element, 'controls');
if (!arr) {
arr = [];
can.data(this.element, 'controls', arr);
}
arr.push(this);
// The `this.options` property is an Object that contains configuration data
// passed to a control when it is created (`new can.Control(element, options)`)
//
// The `options` argument passed when creating the control is merged with `can.Control.defaults`
// in [can.Control.prototype.setup setup].
//
// If no `options` value is used during creation, the value in `defaults` is used instead
this.options = extend({}, cls.defaults, options);
this.on();
return [this.element, this.options];
},
// ## on
//
// This binds an event handler for an event to a selector under the scope of `this.element`
// If no options are specified, all events are rebound to their respective elements. The actions,
// which were cached in `setup`, are used and all elements are bound using `delegate` from `this.element`.
on: function (el, selector, eventName, func) {
if (!el) {
this.off();
var cls = this.constructor,
bindings = this._bindings,
actions = cls.actions,
element = this.element,
destroyCB = can.Control._shifter(this, "destroy"),
funcName, ready;
for (funcName in actions) {
// Only push if we have the action and no option is `undefined`
if ( actions.hasOwnProperty(funcName) ) {
ready = actions[funcName] || cls._action(funcName, this.options, this);
if( ready ) {
bindings.control[funcName] = ready.processor(ready.delegate || element,
ready.parts[2], ready.parts[1], funcName, this);
}
}
}
// Set up the ability to `destroy` the control later.
can.bind.call(element, "removed", destroyCB);
bindings.user.push(function (el) {
can.unbind.call(el, "removed", destroyCB);
});
return bindings.user.length;
}
// if `el` is a string, use that as `selector` and re-set it to this control's element...
if (typeof el === 'string') {
func = eventName;
eventName = selector;
selector = el;
el = this.element;
}
// ...otherwise, set `selector` to null
if (func === undefined) {
func = eventName;
eventName = selector;
selector = null;
}
if (typeof func === 'string') {
func = can.Control._shifter(this, func);
}
this._bindings.user.push(binder(el, eventName, func, selector));
return this._bindings.user.length;
},
// ## off
//
// Unbinds all event handlers on the controller.
// This should _only_ be called in combination with .on()
off: function () {
var el = this.element[0],
bindings = this._bindings;
if( bindings ) {
each(bindings.user || [], function (value) {
value(el);
});
each(bindings.control || {}, function (value) {
value(el);
});
}
// Adds bindings.
this._bindings = {user: [], control: {}};
},
// ## destroy
//
// Prepares a `control` for garbage collection.
// First checks if it has already been removed. Then, removes all the bindings, data, and
// the element from the Control instance.
destroy: function () {
if (this.element === null) {
//!steal-remove-start
can.dev.warn("can/control/control.js: Control already destroyed");
//!steal-remove-end
return;
}
var Class = this.constructor,
pluginName = Class.pluginName || Class._fullName,
controls;
this.off();
if (pluginName && pluginName !== 'can_control') {
this.element.removeClass(pluginName);
}
controls = can.data(this.element, "controls");
controls.splice(can.inArray(this, controls), 1);
can.trigger(this, "destroyed");
this.element = null;
}
});
// ## Processors
//
// Processors do the binding. This basic processor binds events. Each returns a function that unbinds
// when called.
var processors = can.Control.processors;
basicProcessor = function (el, event, selector, methodName, control) {
return binder(el, event, can.Control._shifter(control, methodName), selector);
};
// Set common events to be processed as a `basicProcessor`
each(["change", "click", "contextmenu", "dblclick", "keydown", "keyup",
"keypress", "mousedown", "mousemove", "mouseout", "mouseover",
"mouseup", "reset", "resize", "scroll", "select", "submit", "focusin",
"focusout", "mouseenter", "mouseleave",
"touchstart", "touchmove", "touchcancel", "touchend", "touchleave",
"inserted","removed",
"dragstart", "dragenter", "dragover", "dragleave", "drag", "drop", "dragend"
], function (v) {
processors[v] = basicProcessor;
});
return Control;
});