Created
May 30, 2013 09:59
-
-
Save xiaojue/5676879 to your computer and use it in GitHub Desktop.
ui class
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* @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