magix
Version:
view manager framewrok
776 lines (770 loc) • 26.2 kB
JavaScript
let View_EvtMethodReg = /^(\$?)([^<]*)<([^>]+)>(?:&(.+))?$/;
/*#if(modules.viewProtoMixins){#*/
let processMixinsSameEvent = (exist, additional, temp) => {
if (exist['@{~viewmixin#list}']) {
temp = exist;
} else {
temp = function (e) {
G_ToTry(temp['@{~viewmixin#list}'], e, this);
};
temp['@{~viewmixin#list}'] = [exist];
temp['@{~viewmixin#is.mixin}'] = 1;
}
temp['@{~viewmixin#list}'] = temp['@{~viewmixin#list}'].concat(additional['@{~viewmixin#list}'] || additional);
return temp;
};
/*#}#*/
//let View_MxEvt = /\smx-(?!view|vframe)[a-z]+\s*=\s*"/g;
/*#if(modules.resource){#*/
let View_DestroyAllResources = (me, lastly) => {
let cache = me['@{view#resource}'], //reources
p, c;
for (p in cache) {
c = cache[p];
if (lastly || c.x) { //destroy
View_DestroyResource(cache, p, 1);
}
}
};
let View_DestroyResource = (cache, key, callDestroy, old) => {
let o = cache[key],
fn, res;
if (o && o != old) {
//let processed=false;
res = o.e; //entity
fn = res.destroy;
if (fn && callDestroy) {
G_ToTry(fn, G_EMPTY_ARRAY, res);
}
delete cache[key];
}
return res;
};
/*#}#*/
let View_WrapMethod = (prop, fName, short, fn, me) => {
fn = prop[fName];
prop[fName] = prop[short] = function (...args) {
me = this;
if (me['@{view#sign}'] > 0) { //signature
me['@{view#sign}']++;
/*#if(!modules.mini){#*/
me.fire('rendercall');
/*#}#*/
/*#if(modules.resource){#*/
View_DestroyAllResources(me);
/*#}#*/
/*#if(!modules.keepHTML){#*/
return G_ToTry(fn, args, me);
/*#}else{#*/
return fn.apply(me, args);
/*#}#*/
}
};
};
let View_DelegateEvents = (me, destroy) => {
let e, { '@{view#events.object}': eventsObject,
'@{view#selector.events.object}': selectorObject,
'@{view#events.list}': eventsList, id } = me; //eventsObject
for (e in eventsObject) {
Body_DOMEventBind(e, selectorObject[
e], destroy);
}
for (e of eventsList) {
G_DOMEventLibBind(e.e, e.n, G_DOMGlobalProcessor, destroy, {
i: id,
v: me,
f: e.f,
m: e.m,
e: e.e
});
}
};
let View_Globals = {
win: G_WINDOW,
doc: G_DOCUMENT
};
/*#if(modules.viewProtoMixins||modules.viewMerge){#*/
let View_MergeMixins = (mixins, proto, ctors) => {
let temp = {}, p, node, fn, exist;
for (node of mixins) {
for (p in node) {
fn = node[p];
exist = temp[p];
if (p == 'ctor') {
ctors.push(fn);
continue;
} else if (View_EvtMethodReg.test(p)) {
if (exist) {
fn = processMixinsSameEvent(exist, fn);
} else {
fn['@{~viewmixin#is.mixin}'] = 1;
}
} else if (DEBUG && exist && p != 'extend' && p != G_SPLITER) { //只在开发中提示
Magix_Cfg.error(Error('merge duplicate:' + p));
}
temp[p] = fn;
}
}
for (p in temp) {
if (!G_Has(proto, p)) {
proto[p] = temp[p];
}
}
};
/*#}#*/
function merge(...args) {
let me = this, _ = me._ || (me._ = []);
View_MergeMixins(args, me[G_PROTOTYPE], _);
return me;
}
function extend(props, statics) {
let me = this;
props = props || {};
let ctor = props.ctor;
/*#if(modules.viewProtoMixins){#*/
let ctors = [];
if (ctor) ctors.push(ctor);
/*#}#*/
function NView(nodeId, ownerVf, initParams, node/*#if(modules.viewChildren&&modules.updaterQuick){#*/, vnode/*#}#*//*#if(modules.viewChildren){#*/, parentVf /*#}#*//*#if(modules.viewProtoMixins){#*/, mixinCtors /*#}#*/, cs, z, params/*#if(modules.viewProtoMixins){#*/, concatCtors/*#}#*/) {
me.call(z = this, nodeId, ownerVf, initParams, node/*#if(modules.viewChildren&&modules.updaterQuick){#*/, vnode/*#}#*//*#if(modules.viewChildren){#*/, parentVf/*#}#*//*#if(modules.viewProtoMixins){#*/, mixinCtors/*#}#*/);
cs = NView._;
/*#if(modules.viewProtoMixins){#*/
params = [initParams, {
node,/*#if(modules.viewChildren&&modules.updaterQuick){#*/
vnode,
/*#}#*/
deep: !z.tmpl/*#if(modules.viewChildren){#*/,
map: Children_Wrap(/*#if(modules.updaterQuick){#*/vnode/*#}else{#*/node/*#}#*/, parentVf)/*#}#*/
}];
if (cs) G_ToTry(cs, params, z);
concatCtors = ctors.concat(mixinCtors);
if (concatCtors.length) {
G_ToTry(concatCtors, params, z);
}
/*#}else{#*/
params = {
node,/*#if(modules.viewChildren&&modules.updaterQuick){#*/
vnode,
/*#}#*/
deep: !z.tmpl/*#if(modules.viewChildren){#*/,
map: Children_Wrap(/*#if(modules.updaterQuick){#*/vnode/*#}else{#*/node/*#}#*/, parentVf)/*#}#*/
};
if (cs) G_ToTry(cs, [initParams, params], z);
if (ctor) ctor.call(z, initParams, params);
/*#}#*/
}
NView.merge = merge;
NView.extend = extend;
return G_Extend(NView, me, props, statics);
}
/**
* 预处理view
* @param {View} oView view子类
* @param {Vom} vom vom
*/
let View_Prepare = oView => {
if (!oView[G_SPLITER]) { //只处理一次
oView[G_SPLITER] = /*#if(modules.viewProtoMixins){#*/[] /*#}else{#*/ 1 /*#}#*/;
let prop = oView[G_PROTOTYPE],
currentFn, matches, selectorOrCallback, events, eventsObject = {},
eventsList = [],
selectorObject = {},
node, isSelector, p, item, mask, mod, modifiers;
/*#if(modules.viewProtoMixins){#*/
matches = prop.mixins;
if (matches) {
View_MergeMixins(matches, prop, oView[G_SPLITER]);
}
/*#}#*/
for (p in prop) {
currentFn = prop[p];
matches = p.match(View_EvtMethodReg);
if (matches) {
[, isSelector, selectorOrCallback, events, modifiers] = matches;
mod = {};
if (modifiers) {
modifiers = modifiers.split(G_COMMA);
for (item of modifiers) {
mod[item] = true;
}
}
events = events.split(G_COMMA);
for (item of events) {
node = View_Globals[selectorOrCallback];
mask = 1;
if (isSelector) {
if (node) {
eventsList.push({
f: currentFn,
e: node,
n: item,
m: mod
});
continue;
}
mask = 2;
node = selectorObject[item];
if (!node) {
node = selectorObject[item] = [];
}
if (!node[selectorOrCallback]) {
node[selectorOrCallback] = 1;
node.push(selectorOrCallback);
}
}
eventsObject[item] = eventsObject[item] | mask;
item = selectorOrCallback + G_SPLITER + item;
node = prop[item];
/*#if(modules.viewProtoMixins){#*/
//for in 就近遍历,如果有则忽略
if (!node) { //未设置过
prop[item] = currentFn;
} else if (node['@{~viewmixin#is.mixin}']) { //现有的方法是mixins上的
if (currentFn['@{~viewmixin#is.mixin}']) { //2者都是mixins上的事件,则合并
prop[item] = processMixinsSameEvent(currentFn, node);
} else if (G_Has(prop, p)) { //currentFn方法不是mixin上的,也不是继承来的,在当前view上,优先级最高
prop[item] = currentFn;
}
}
/*#}else{#*/
if (!node) {
prop[item] = currentFn;
}
/*#}#*/
}
}
}
//console.log(prop);
View_WrapMethod(prop, 'render', '@{view#render.short}');
prop['@{view#events.object}'] = eventsObject;
prop['@{view#events.list}'] = eventsList;
prop['@{view#selector.events.object}'] = selectorObject;
prop['@{view#assign.fn}'] = prop.assign;
}
/*#if(modules.viewProtoMixins){#*/
return oView[G_SPLITER];
/*#}#*/
};
/*#if(modules.router){#*/
let View_IsObserveChanged = view => {
let loc = view['@{view#observe.router}'];
let res, i, params;
if (loc.f) {
if (loc.p) {
res = Router_LastChanged[G_PATH];
}
if (!res && loc.k) {
params = Router_LastChanged[G_PARAMS];
for (i of loc.k) {
res = G_Has(params, i);
if (res) break;
}
}
// if (res && loc.c) {
// loc.c.call(view);
// }
}
return res;
};
/*#}#*/
/**
* View类
* @name View
* @class
* @constructor
* @borrows Event.on as #on
* @borrows Event.fire as #fire
* @borrows Event.off as #off
* @param {Object} ops 创建view时,需要附加到view对象上的其它属性
* @property {String} id 当前view与页面vframe节点对应的id
* @property {Vframe} owner 拥有当前view的vframe对象
* @example
* // 关于事件:
* // html写法:
*
* // <input type="button" mx-click="test({id:100,name:'xinglie'})" value="test" />
* // <a href="http://etao.com" mx-click="test({com:'etao.com'})">http://etao.com</a>
*
* // js写法:
*
* 'test<click>':function(e){
* e.preventDefault();
* //e.current 处理事件的dom节点(即带有mx-click属性的节点)
* //e.target 触发事件的dom节点(即鼠标点中的节点,在current里包含其它节点时,current与target有可能不一样)
* //e.params 传递的参数
* //e.params.com,e.params.id,e.params.name
* },
* 'test<mousedown>':function(e){
*
* }
*
* //上述示例对test方法标注了click与mousedown事件,也可以合写成:
* 'test<click,mousedown>':function(e){
* alert(e.type);//可通过type识别是哪种事件类型
* }
*/
function View(id, owner, ops, node, me) {
me = this;
me.owner = owner;
me.id = id;
/*#if(modules.router){#*/
me['@{view#observe.router}'] = {
k: []
};
/*#}#*/
/*#if(modules.resource){#*/
me['@{view#resource}'] = {};
/*#}#*/
me['@{view#sign}'] = 1; //标识view是否刷新过,对于托管的函数资源,在回调这个函数时,不但要确保view没有销毁,而且要确保view没有刷新过,如果刷新过则不回调
/*#if(modules.updater){#*/
me.updater = me['@{view#updater}'] = new Updater(me.id);
/*#}#*/
/*#if(modules.viewMerge){#*/
id = View._;
if (id) G_ToTry(id, [ops, {
node,
deep: !me.tmpl
}], me);
/*#}#*/
}
G_Assign(View, {
/**
* @lends View
*/
/**
* 扩展View
* @param {Object} props 扩展到原型上的方法
* @example
* define('app/tview',function(require){
* let Magix = require('magix');
* Magix.View.merge({
* ctor:function(){
* this.$attr='test';
* },
* test:function(){
* alert(this.$attr);
* }
* });
* });
* //加入Magix.config的exts中
*
* Magix.config({
* //...
* exts:['app/tview']
*
* });
*
* //这样完成后,所有的view对象都会有一个$attr属性和test方法
* //当然上述功能也可以用继承实现,但继承层次太多时,可以考虑使用扩展来消除多层次的继承
* //同时当项目进行中发现所有view要实现某个功能时,该方式比继承更快捷有效
*
*
*/
/*#if(modules.viewMerge){#*/
merge,
/*#}#*/
/**
* 继承
* @param {Object} [props] 原型链上的方法或属性对象
* @param {Function} [props.ctor] 类似constructor,但不是constructor,当我们继承时,你无需显示调用上一层级的ctor方法,magix会自动帮你调用
* @param {Array} [props.mixins] mix到当前原型链上的方法对象,该对象可以有一个ctor方法用于初始化
* @param {Object} [statics] 静态对象或方法
* @example
* let Magix = require('magix');
* let Sortable = {
* ctor: function() {
* console.log('sortable ctor');
* //this==当前mix Sortable的view对象
* this.on('destroy', function() {
* console.log('dispose')
* });
* },
* sort: function() {
* console.log('sort');
* }
* };
* module.exports = Magix.View.extend({
* mixins: [Sortable],
* ctor: function() {
* console.log('view ctor');
* },
* render: function() {
* this.sort();
* }
* });
*/
extend
});
G_Assign(View[G_PROTOTYPE] /*#if(!modules.mini){#*/, MEvent/*#}#*/, {
/**
* @lends View#
*/
/*#if(modules.viewInit){#*/
/**
* 初始化调用的方法
* @beta
* @module viewInit
* @param {Object} extra 外部传递的数据对象
*/
init: G_NOOP,
/*#}#*/
/*
* 包装mx-event事件,比如把mx-click="test<prevent>({key:'field'})" 包装成 mx-click="magix_vf_root^test<prevent>({key:'field})",以方便识别交由哪个view处理
* @function
* @param {String} html 处理的代码片断
* @param {Boolean} [onlyAddPrefix] 是否只添加前缀
* @return {String} 处理后的字符串
* @example
* View.extend({
* 'del<click>':function(e){
* S.one(G_HashKey+e.currentId).remove();
* },
* 'addNode<click>':function(e){
* let tmpl='<div mx-click="del">delete</div>';
* //因为tmpl中有mx-click,因此需要下面这行代码进行处理一次
* tmpl=this.wrapEvent(tmpl);
* S.one(G_HashKey+e.currentId).append(tmpl);
* }
* });
*/
/**
* 通知当前view即将开始进行html的更新
* @param {String} [id] 哪块区域需要更新,默认整个view
*/
beginUpdate(id, me) {
me = this;
if (me['@{view#sign}'] > 0 && me['@{view#rendered}']) {
me.owner.unmountZone(id, 1);
/*me.fire('prerender', {
id: id
});*/
}
},
/**
* 通知当前view结束html的更新
* @param {String} [id] 哪块区域结束更新,默认整个view
*/
endUpdate(id, inner, me /*#if(modules.linkage){#*/, o, f /*#}#*/) {
me = this;
if (me['@{view#sign}'] > 0) {
id = id || me.id;
/*me.fire('rendered', {
id
});*/
if (inner) {
f = inner;
} else {
/*#if(modules.linkage){#*/
f = me['@{view#rendered}'];
/*#}#*/
me['@{view#rendered}'] = 1;
}
/*#if(modules.linkage){#*/
o = me.owner;
o.mountZone(id, inner);
if (!f) {
/*#if(modules.es3){#*/
Timeout(me.wrapAsync(() => {
Vframe_RunInvokes(o);
}), 0);
/*#}else{#*/
Timeout(me.wrapAsync(Vframe_RunInvokes), 0, o);
/*#}#*/
}
/*#}else{#*/
me.owner.mountZone(id, inner);
/*#}#*/
}
},
/*#if(!modules.mini){#*/
/**
* 包装异步回调
* @param {Function} fn 异步回调的function
* @return {Function}
* @example
* render:function(){
* setTimeout(this.wrapAsync(function(){
* //codes
* }),50000);
* }
* // 为什么要包装一次?
* // 在单页应用的情况下,可能异步回调执行时,当前view已经被销毁。
* // 比如上例中的setTimeout,50s后执行回调,如果你的回调中去操作了DOM,
* // 则会出错,为了避免这种情况的出现,可以调用view的wrapAsync包装一次。
* // (该示例中最好的做法是在view销毁时清除setTimeout,
* // 但有时候你很难控制回调的执行,比如JSONP,所以最好包装一次)
*/
wrapAsync(fn, context) {
let me = this;
let sign = me['@{view#sign}'];
return (...a) => {
if (sign > 0 && sign == me['@{view#sign}']) {
return fn.apply(context || me, a);
}
};
},
/*#}#*/
/*#if(modules.router){#*/
/**
* 监视地址栏中的参数或path,有变动时,才调用当前view的render方法。通常情况下location有变化不会引起当前view的render被调用,所以你需要指定地址栏中哪些参数有变化时才引起render调用,使得view只关注与自已需要刷新有关的参数
* @param {Array|String|Object} params 数组字符串
* @param {Boolean} [isObservePath] 是否监视path
* @beta
* @module router
* @example
* return View.extend({
* init:function(){
* this.observeLocation('page,rows');//关注地址栏中的page rows2个参数的变化,当其中的任意一个改变时,才引起当前view的render被调用
* this.observeLocation(null,true);//关注path的变化
* //也可以写成下面的形式
* //this.observeLocation('page,rows',true);
* //也可以是对象的形式
* this.observeLocation({
* path: true,
* params:['page','rows']
* });
* },
* render:function(){
* let loc=Magix.Router.parse();
* console.log(loc);//获取地址解析出的对象
* let diff=Magix.Router.diff();
* console.log(diff);//获取当前地址与上一个地址差异对象
* }
* });
*/
observeLocation(params, isObservePath) {
let me = this,
loc;
loc = me['@{view#observe.router}'];
loc.f = 1;
if (G_IsObject(params)) {
isObservePath = params[G_PATH];
params = params[G_PARAMS];
}
//if (isObservePath) {
loc.p = isObservePath;
//}
if (params) {
loc.k = (params + G_EMPTY).split(G_COMMA);
}
},
/*#}#*/
/*#if(modules.state){#*/
/**
* 监视Magix.State中的数据变化
* @param {String|Array} keys 数据对象的key
*/
observeState(keys) {
this['@{view#observe.state}'] = (keys + G_EMPTY).split(G_COMMA);
},
/*#}#*/
/*#if(modules.resource){#*/
/**
* 让view帮你管理资源,强烈建议对组件等进行托管
* @param {String} key 资源标识key
* @param {Object} res 要托管的资源
* @param {Boolean} [destroyWhenCalleRender] 调用render方法时是否销毁托管的资源
* @return {Object} 返回托管的资源
* @beta
* @module resource
* @example
* View.extend({
* render: function(){
* let me = this;
* let dropdown = new Dropdown();
*
* me.capture('dropdown',dropdown,true);
* },
* getTest: function(){
* let dd = me.capture('dropdown');
* console.log(dd);
* }
* });
*/
capture(key, res, destroyWhenCallRender, cache) {
cache = this['@{view#resource}'];
if (res) {
View_DestroyResource(cache, key, 1, res);
cache[key] = {
e: res,
x: destroyWhenCallRender
};
//service托管检查
if (DEBUG && res && (res.id + G_EMPTY).indexOf('\x1es') === 0) {
res['@{service#captured}'] = 1;
if (!destroyWhenCallRender) {
console.warn('beware! May be you should set destroyWhenCallRender = true');
}
}
} else {
cache = cache[key];
res = cache && cache.e;
}
return res;
},
/**
* 释放管理的资源
* @param {String} key 托管时的key
* @param {Boolean} [destroy] 是否销毁资源
* @return {Object} 返回托管的资源,无论是否销毁
* @beta
* @module resource
*/
release(key, destroy) {
return View_DestroyResource(this['@{view#resource}'], key, destroy);
},
/*#}#*/
/*#if(modules.tipRouter){#*/
/**
* 离开提示
* @param {String} msg 提示消息
* @param {Function} fn 是否提示的回调
* @beta
* @module tipRouter
* @example
* let Magix = require('magix');
* module.exports = Magix.View.extend({
* init:function(){
* this.leaveTip('页面数据未保存,确认离开吗?',function(){
* return true;//true提示,false,不提示
* });
* }
* });
*/
leaveTip(msg, fn) {
let me = this;
let changeListener = e => {
let a = '@{~tip#router.change}', // a for router change
b = '@{~tip#view.unload}'; // b for viewunload change
if (e.type != G_CHANGE) {
a = '@{~tip#view.unload}';
b = '@{~tip#router.change}';
}
if (changeListener[a]) {
e.prevent();
e.reject();
} else if (fn()) {
e.prevent();
changeListener[b] = 1;
me.leaveConfirm(() => {
changeListener[b] = 0;
e.resolve();
}, () => {
changeListener[b] = 0;
e.reject();
}, msg);
}
};
let unloadListener = e => {
if (fn()) {
e.msg = msg;
}
};
Router.on(G_CHANGE, changeListener);
Router.on(G_PAGE_UNLOAD, unloadListener);
me.on('unload', changeListener);
me.on('destroy', () => {
Router.off(G_CHANGE, changeListener);
Router.off(G_PAGE_UNLOAD, unloadListener);
});
},
/*#}#*/
/*#if(modules.share){#*/
/**
* 向子(孙)view公开数据
* @param {String} key key
* @param {Object} data 数据
* @beta
* @module share
*/
share(key, data) {
let me = this;
if (!me['@{view#shared.data}']) {
me['@{view#shared.data}'] = {};
}
me['@{view#shared.data}'][key] = data;
},
/**
* 获取祖先view上公开的数据
* @param {String} key key
* @return {Object}
* @beta
* @module share
* @example
* //父view
* render:function(){
* this.share('x',{a:20});
* }
* //子view
* render:function(){
* let d=this.getShared('x');
* }
*/
getShared(key) {
let me = this;
let sd = me['@{view#shared.data}'];
let exist;
if (sd) {
exist = G_Has(sd, key);
if (exist) {
return sd[key];
}
}
let vf = me.owner.parent();
if (vf) {
return vf.invoke('getShared', key);
}
},
/*#}#*/
/**
* 设置view的html内容
* @param {String} id 更新节点的id
* @param {Strig} html html字符串
* @example
* render:function(){
* this.setHTML(this.id,this.tmpl);//渲染界面,当界面复杂时,请考虑用其它方案进行更新
* }
*/
/*
Q:为什么删除setHTML?
A:统一使用updater更新界面。
关于api的分级,高层api更内聚,一个api完成很多功能。方便开发者,但不灵活。
底层api职责更单一,一个api只完成一个功能,灵活,但不方便开发者
更新界面来讲,updater是一个高层api,但是有些功能却无法完成,如把view当成壳子或容器渲染第三方的组件,组件什么时间加载完成、渲染、更新了dom、如何通知magix等,这些问题在updater中是无解的,而setHTML这个api又不够底层,同样也无法完成一些功能,所以这个api食之无味,故删除
*/
/*setHTML(id, html) {
let me = this,
n, i = me.id;
me.beginUpdate(id);
if (me['@{view#sign}'] > 0) {
n = G_GetById(id);
if (n) G_HTML(n, View_SetEventOwner(html, i), i);
}
me.endUpdate(id);
me.fire('domready');
}*/
/**
* 渲染view,供最终view开发者覆盖
* @function
*/
render: G_NOOP
/**
* 当前view的dom就绪后触发
* @name View#domready
* @event
* @param {Object} e view 完成渲染后触发
*/
/**
* view销毁时触发
* @name View#destroy
* @event
* @param {Object} e
*/
/**
* 异步更新ui的方法(render)被调用前触发
* @name View#rendercall
* @event
* @param {Object} e
*/
});
Magix.View = View;