Skip to content

Instantly share code, notes, and snippets.

@xiaojue
Created May 30, 2013 09:59
Show Gist options
  • Save xiaojue/5676879 to your computer and use it in GitHub Desktop.
Save xiaojue/5676879 to your computer and use it in GitHub Desktop.
ui class
/*
* @UI class
* @author [email protected]
**/
(function() {
//class类,所有UI组件均基于class创建实例.
function Class(o) {
//把一个普通类,转换为符合Class的格式的类
if (! (this instanceof Class) && sjs.isFunction(o)) {
return classify(o);
}
}
Class.create = function(parent, properties) {
if (!sjs.isFunction(parent)) {
properties = parent;
parent = null;
}
properties || (properties = {});
parent || (parent = properties.Extends || Class);
properties.Extends = parent;
function SubClass() {
var arg = sjs.makeArray(arguments);
parent.apply(this, arg);
if (this.constructor === SubClass && this.initialize) {
this.initialize.apply(this, arg);
}
}
if (parent !== Class) {
mix(SubClass, parent, parent.StaticsWhiteList);
}
implement.call(SubClass, properties);
return classify(SubClass);
};
Class.Mutators = {
'Extends': function(parent) {
var existed = this.prototype,
proto = createProto(parent.prototype);
mix(proto, existed);
proto.constructor = this;
this.prototype = proto;
this.superclass = parent.prototype;
},
'Implements': function(items) {
items = sjs.isArray(items) ? items: [items];
var proto = this.prototype,
item;
while (items.length) {
item = items.shift();
mix(proto, item.prototype || item);
}
},
'Statics': function(staticProperties) {
mix(this, staticProperties);
}
};
//创建一个基于class的超级类
Class.extend = function(properties) {
properties || (properties = {});
properties.Extends = this;
return Class.create(properties);
};
function implement(properties) {
var self = this;
sjs.each(properties, function(key, value) {
if (Class.Mutators.hasOwnProperty(key)) {
Class.Mutators[key].call(self, value);
} else {
self.prototype[key] = value;
}
});
}
//转换普通类
function classify(cls) {
cls.extend = Class.extend;
cls.implement = implement;
return cls;
}
function Ctor() {}
var createProto = Object['__proto__'] ? function(proto) {
return {
'__proto__': proto
};
}: function(proto) {
Ctor.prototype = proto;
return new Ctor();
};
function mix(r, s, wl) {
for (var p in s) {
if (s.hasOwnProperty(p)) {
if (wl && sjs.inArray(p, wl) === - 1) {
continue;
}
// 在 iPhone 1 代等设备的 Safari 中,prototype 也会被枚举出来,需排除
if (p !== 'prototype') {
r[p] = s[p];
}
}
}
}
function getMethod(host, methodName) {
var method = host[methodName];
if (!method) {
throw new Error('Invalid method name:' + methodName);
}
return method;
}
function wrap(methodName) {
var old = this[methodName];
this[methodName] = function() {
var args = sjs.makeArray(arguments);
var beforeArgs = ['before:' + methodName].concat(args);
this.trigger.apply(this, beforeArgs);
var beforeCallback = this.proxy_events['before:' + methodName];
if (beforeCallback && beforeCallback.callbacks.locked()) {
return;
}
var ret = old.apply(this, args);
var afterArgs = ['after:' + methodName, ret].concat(args);
this.trigger.apply(this, afterArgs);
};
this[methodName].__isAspected = true;
}
var proxy_event = {
once: function(events, callback, context) {
this.on(events, callback, context, true);
},
on: function(events, callback, context, once) {
var proxy_events = this.proxy_events || (this.proxy_events = {});
if (!proxy_events.hasOwnProperty(events)) {
var callbacks = once ? sjs.Callbacks('once stopOnFalse') : sjs.Callbacks('stopOnFalse');
proxy_events[events] = {
callbacks: callbacks,
context: context
};
}
proxy_events[events].callbacks.add(callback);
},
off: function(events, callback) {
var proxy_events = this.proxy_events;
if (proxy_events) {
var event = proxy_events[events];
if (events && event) {
var callbacks = event.callbacks;
if (callback) {
callbacks.remove(callback);
} else {
callbacks.empty();
}
} else {
for (var i in proxy_events) {
proxy_events[i].callbacks.empty();
}
}
}
},
trigger: function() { //[name,args]
if (this.proxy_events) {
var args = sjs.makeArray(arguments),
name = args.shift();
proxy_event = this.proxy_events[name];
if (proxy_event) {
var callbacks = proxy_event.callbacks,
context = proxy_event.context;
callbacks.fire.apply((context ? sjs.extend(context, callbacks) : false) || callbacks, args);
}
}
},
before: function(methodName, callback, context) {
var method = getMethod(this, methodName);
if (!method.__isAspected) {
wrap.call(this, methodName);
}
this.on('before:' + methodName, callback, context);
return this;
},
after: function(methodName, callback, context) {
var method = getMethod(this, methodName);
if (!method.__isAspected) {
wrap.call(this, methodName);
}
this.on('after:' + methodName, callback, context);
return this;
}
};
var proxy_attribute = {
initAttrs: function(config) {
// initAttrs 是在初始化时调用的,默认情况下实例上肯定没有 attrs,不存在覆盖问题
var attrs = this.attrs = {};
// Get all inherited attributes.
var specialProps = this.propsInAttrs || [];
mergeInheritedAttrs(attrs, this, specialProps);
// Merge user-specific attributes from config.
if (config) {
mergeUserValue(attrs, config);
}
// 对于有 setter 的属性,要用初始值 set 一下,以保证关联属性也一同初始化
setSetterAttrs(this, attrs, config);
// Convert `on/before/afterXxx` config to event handler.
parseEventsFromAttrs(this, attrs);
// 将 this.attrs 上的 special properties 放回 this 上
copySpecialProps(specialProps, this, attrs, true);
},
get: function(key) {
var attr = this.attrs[key] || {};
var val = attr.value;
return attr.getter ? attr.getter.call(this, val, key) : val;
},
set: function(key, val, options) {
var attrs = {};
// set("key", val, options)
if (sjs.isString(key)) {
attrs[key] = val;
}
// set({ "key": val, "key2": val2 }, options)
else {
attrs = key;
options = val;
}
options || (options = {});
var silent = options.silent;
var override = options.override;
var now = this.attrs;
var changed = this.__changedAttrs || (this.__changedAttrs = {});
for (key in attrs) {
if (!attrs.hasOwnProperty(key)) {
continue;
}
var attr = now[key] || (now[key] = {});
val = attrs[key];
if (attr.readOnly) {
throw new Error('This attribute is readOnly: ' + key);
}
// invoke setter
if (attr.setter) {
val = attr.setter.call(this, val, key);
}
// 获取设置前的 prev 值
var prev = this.get(key);
// 获取需要设置的 val 值
// 如果设置了 override 为 true,表示要强制覆盖,就不去 merge 了
// 都为对象时,做 merge 操作,以保留 prev 上没有覆盖的值
if (!override && sjs.isPlainObject(prev) && sjs.isPlainObject(val)) {
val = merge(merge({},
prev), val);
}
// set finally
now[key].value = val;
// invoke change event
// 初始化时对 set 的调用,不触发任何事件
if (!this.__initializingAttrs && ! sjs.isEqual(prev, val)) {
if (silent) {
changed[key] = [val, prev];
}
else {
this.trigger('change:' + key, val, prev, key);
}
}
}
return this;
},
change: function() {
var changed = this.__changedAttrs;
if (changed) {
for (var key in changed) {
if (changed.hasOwnProperty(key)) {
var args = changed[key];
this.trigger('change:' + key, args[0], args[1], key);
}
}
delete this.__changedAttrs;
}
return this;
}
};
//attributes help
function merge(receiver, supplier) {
var key, value;
for (key in supplier) {
if (supplier.hasOwnProperty(key)) {
value = supplier[key];
// 只 clone 数组和 plain object,其他的保持不变
if (sjs.isArray(value)) {
value = value.slice();
}
else if (sjs.isPlainObject(value)) {
var prev = receiver[key];
sjs.isPlainObject(prev) || (prev = {});
value = merge(prev, value);
}
receiver[key] = value;
}
}
return receiver;
}
function mergeInheritedAttrs(attrs, instance, specialProps) {
var inherited = [];
var proto = instance.constructor.prototype;
while (proto) {
// 不要拿到 prototype 上的
if (!proto.hasOwnProperty('attrs')) {
proto.attrs = {};
}
// 将 proto 上的特殊 properties 放到 proto.attrs 上,以便合并
copySpecialProps(specialProps, proto.attrs, proto);
// 为空时不添加
if (!sjs.isEmptyObject(proto.attrs)) {
inherited.unshift(proto.attrs);
}
// 向上回溯一级
proto = proto.constructor.superclass;
}
// Merge and clone default values to instance.
for (var i = 0, len = inherited.length; i < len; i++) {
merge(attrs, normalize(inherited[i]));
}
}
function copySpecialProps(specialProps, receiver, supplier, isAttr2Prop) {
for (var i = 0, len = specialProps.length; i < len; i++) {
var key = specialProps[i];
if (supplier.hasOwnProperty(key)) {
receiver[key] = isAttr2Prop ? receiver.get(key) : supplier[key];
}
}
}
function mergeUserValue(attrs, config) {
merge(attrs, normalize(config, true));
}
function setSetterAttrs(host, attrs, config) {
var options = {
silent: true
};
host.__initializingAttrs = true;
for (var key in config) {
if (config.hasOwnProperty(key)) {
if (attrs[key].setter) {
host.set(key, config[key], options);
}
}
}
delete host.__initializingAttrs;
}
var EVENT_PATTERN = /^(on|before|after)([A-Z].*)$/;
var EVENT_NAME_PATTERN = /^(Change)?([A-Z])(.*)/;
function parseEventsFromAttrs(host, attrs) {
for (var key in attrs) {
if (attrs.hasOwnProperty(key)) {
var value = attrs[key].value,
m;
if (sjs.isFunction(value) && (m = key.match(EVENT_PATTERN))) {
host[m[1]](getEventName(m[2]), value);
delete attrs[key];
}
}
}
}
// Converts `Show` to `show` and `ChangeTitle` to `change:title`
function getEventName(name) {
var m = name.match(EVENT_NAME_PATTERN);
var ret = m[1] ? 'change:': '';
ret += m[2].toLowerCase() + m[3];
return ret;
}
var ATTR_SPECIAL_KEYS = ['value', 'getter', 'setter', 'readOnly'];
// normalize `attrs` to
//
// {
// value: 'xx',
// getter: fn,
// setter: fn,
// readOnly: boolean
// }
//
function normalize(attrs, isUserValue) {
var newAttrs = {};
for (var key in attrs) {
var attr = attrs[key];
if (!isUserValue && sjs.isPlainObject(attr) && hasOwnProperties(attr, ATTR_SPECIAL_KEYS)) {
newAttrs[key] = attr;
continue;
}
newAttrs[key] = {
value: attr
};
}
return newAttrs;
}
function hasOwnProperties(object, properties) {
for (var i = 0, len = properties.length; i < len; i++) {
if (object.hasOwnProperty(properties[i])) {
return true;
}
}
return false;
}
// 对于 attrs 的 value 来说,以下值都认为是空值: null, undefined, '', [], {}
function isEmptyAttrValue(o) {
return o === null || // null, undefined
(sjs.isString(o) || sjs.isArray(o)) && o.length === 0 || // '', []
sjs.isEmptyObject(o); // {}
}
function parseEventsFromInstance(host, attrs) {
for (var attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
var m = '_onChange' + ucfirst(attr);
if (host[m]) {
host.on('change:' + attr, host[m]);
}
}
}
}
function ucfirst(str) {
return str.charAt(0).toUpperCase() + str.substring(1);
}
var Base = Class.create({
Implements: [proxy_event, proxy_attribute],
initialize: function(config) {
//初始化Attribute方法和参数
this.initAttrs(config);
// Automatically register `this._onChangeAttr` method as
// a `change:attr` event handler.
parseEventsFromInstance(this, this.attrs);
},
destroy: function() {
this.off();
for (var p in this) {
if (this.hasOwnProperty(p)) {
delete this[p];
}
}
}
});
//UI helper
var cidCounter = 0;
var cachedInstances = [];
var DATA_UI_CID = 'data-ui-cid';
var EVENT_KEY_SPLITTER = /^(\S+)\s*(.*)$/;
var ON_RENDER = '_onRender';
var DELEGATE_EVENT_NS = '.delegate-events-';
function uniqueCid() {
cidCounter++;
return 'ui-' + cidCounter;
}
function getEvents(ui) {
if (sjs.isFunction(ui.events)) {
ui.events = ui.events();
}
return ui.events;
}
function parseEventKey(eventKey, ui) {
var match = eventKey.match(EVENT_KEY_SPLITTER);
var eventType = match[1] + DELEGATE_EVENT_NS + ui.cid;
var selector = match[2] || undefined;
return {
type: eventType,
selector: selector
};
}
function isInDocument(element) {
return sjs.contains(document.documentElement, element);
}
var UI = Base.extend({
propsInAttrs: ['initElement', 'element', 'events'],
element: null,
events: null,
attrs: {
id: null,
className: null,
style: null,
template: '<div></div>',
model: null,
parentNode: document.body
},
initialize: function(config) {
//生成唯一id
this.cid = uniqueCid();
//初始化attrs
UI.superclass.initialize.call(this, config || {});
//初始化props
this.parseElement();
this.initProps();
//初始化events
this.delegateEvents();
//子类自定义初始化
this.setup();
//保存实例信息
this._stamp();
//是否由template初始化
this._isTemplate = ! (config && config.element);
},
//构建this.element
parseElement: function() {
var element = this.element;
if (element) {
this.element = sjs(element);
} else if (this.get('template')) {
this.parseElementFromTemplate();
}
if (!this.element || ! this.element[0]) {
throw new Error('element is invalid');
}
},
parseElementFromTemplate: function() {
this.element = sjs(this.get('template'));
},
initProps: function() {
},
delegateEvents: function(element, events, handler) {
//UI.delegateEvents();
if (arguments.length === 0) {
events = getEvents(this);
element = this.element;
}
//UI.delegateEvents({
// 'click p':'fn1',
// 'click li':'fn2'
//});
else if (arguments.length === 1) {
events = element;
element = this.element;
}
// UI.delegateEvents('click p',function(ev){ ... });
else if (arguments.length === 2) {
handler = events;
events = element;
element = this.element;
}
// UI.delegateEvents(element,'click p',function(ev){...});
else {
element || (element = this.element);
this._delegateElements || (this._delegateElements = []);
this._delegateElements.push(element);
}
//'click p' -> {'click p':handler}
if (sjs.isString(events) && sjs.isFunction(handler)) {
var o = {};
o[events] = handler;
events = o;
}
function inner(handler, ui) {
var callback = function(ev) {
if (sjs.isFunction(handler)) {
handler.call(ui, ev);
} else {
ui[handler](ev);
}
};
// delegate
if (selector) {
sjs(element).on(eventType, selector, callback);
}
}
//key 为 event selector
for (var key in events) {
if (!events.hasOwnProperty(key)) {
continue;
}
var args = parseEventKey(key, this);
var eventType = args.type;
var selector = args.selector;
inner(events[key], this);
}
return this;
},
undelegateEvents: function(element, eventKey) {
if (!eventKey) {
eventKey = element;
element = null;
}
//卸载所有
//.undelegateEvents();
if (arguments.length === 0) {
var type = DELEGATE_EVENT_NS + this.cid;
this.element && this.element.off(type);
//卸载外部element
if (this._delegateElements) {
for (var de in this._delegateElements) {
if (!this._delegateElements.hasOwnProperty(de)) {
continue;
}
this._delegateElements[de].off(type);
}
}
} else {
var args = parseEventKey(eventKey, this);
//卸载this.element
// .undelegateEvents(events);
if (!element) {
this.element && this.element.off(args.type, args.selector);
}
//卸载外部element
// .undelegateEvents(element,events)
else {
sjs(element).off(args.type, args.selector);
}
}
return this;
},
setup: function() {
},
//子类覆盖时也要 return this;
render: function() {
if (!this.rendered) {
//初始化,并把相关属性帮顶到change
this._renderAttrBindAttrs();
this.rendered = true;
}
//插入节点
var parentNode = this.get('parentNode');
if (parentNode && ! isInDocument(this.element[0])) {
// 隔离样式,添加统一的命名空间
// 只处理 template 的情况,不处理传入的 element
var outerBoxClass = this.constructor.outerBoxClass;
if (outerBoxClass) {
var outerBox = this._outerBox = sjs('<div></div>').addClass(outerBoxClass);
outerBox.append(this.element).appendTo(parentNode);
} else {
this.element.appendTo(parentNode);
}
}
return this;
},
_renderAttrBindAttrs: function() {
var ui = this;
var attrs = ui.attrs;
function inner(m) {
ui.on('change:' + attr, function(val, prev, key) {
ui[m](val, prev, key);
});
}
for (var attr in attrs) {
if (!attrs.hasOwnProperty(attr)) {
continue;
}
var m = ON_RENDER + ucfirst(attr);
if (this[m]) {
var val = this.get(attr);
// 让属性的初始值生效。注:默认空值不触发
if (!isEmptyAttrValue(val)) {
this[m](val, undefined, attr);
}
// 将 _onRenderXx 自动绑定到 change:xx 事件上
inner(m);
}
}
},
_onRenderId: function(val) {
this.element.attr('id', val);
},
_onRenderClassName: function(val) {
this.element.addClass(val);
},
_onRenderStyle: function(val) {
this.element.css(val);
},
_stamp: function() {
var cid = this.cid;
this.element.attr(DATA_UI_CID, cid);
cachedInstances[cid] = this;
},
$: function(selector) {
return this.element.find(selector);
},
destroy: function() {
this.undelegateEvents();
delete cachedInstances[this.cid];
//防止内存泄露
if (this.element) {
this.element.off();
// 如果是 ui 生成的 element 则去除
this._isTemplate && (this._outerBox || this.element).remove();
this.element = null;
}
UI.superclass.destroy.call(this);
}
});
//防止内存泄露
sjs(window).unload(function() {
for (var cid in cachedInstances) {
cachedInstances[cid].destroy();
}
});
UI.query = function(selector) {
var element = sjs(selector).eq(0);
var cid;
element && (cid = element.attr(DATA_UI_CID));
return cachedInstances[cid];
};
sjs.UI = UI;
//测试用
sjs.UI.Class = Class;
sjs.UI.Base = Base;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment