import { NEvent } from "../core/event";
import { GlobalCache } from "../core/globalcache";
import { Model } from "../core/model";
import { ModelManager } from "../core/modelmanager";
import { Module } from "../core/module";
import { ModuleFactory } from "../core/modulefactory";
import { createDirective } from "../core/nodom";
import { Renderer } from "../core/renderer";
import { Router } from "../core/router";
import { IRenderedDom } from "../core/types";
import { Util } from "../core/util";
import { VirtualDom } from "../core/virtualdom";
export default (function () {
/**
* 指令类型初始化
* 每个指令类型都有一个名字、处理函数和优先级,处理函数不能用箭头函数
* 处理函数在渲染时执行,包含两个参数 module(模块)、dom(目标虚拟dom)、src(源虚拟dom)
* 处理函数的this指向指令
* 处理函数的返回值 true 表示继续,false 表示后续指令不再执行
*/
/**
* module 指令
* 用于指定该元素为模块容器,表示子模块
* 用法 x-module='模块类名'
*/
createDirective(
'module',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
let m: Module;
//存在moduleId,表示已经渲染过,不渲染
let mid = module.objectManager.getDomParam(dom.key, 'moduleId');
let handle: boolean = true;
if (mid) {
m = ModuleFactory.get(mid);
handle = !dom.props['renderOnce'];
} else {
m = ModuleFactory.get(this.value);
if (!m) {
return true;
}
mid = m.id;
//保留modelId
module.objectManager.setDomParam(dom.key, 'moduleId', mid);
module.addChild(m);
//共享当前dom的model给子模块
if (src.hasProp('useDomModel')) {
m.model = dom.model;
//绑定model到子模块,共享update,watch方法
ModelManager.bindToModule(m.model, m);
delete dom.props['useDomModel'];
}
}
//保存到dom上,提升渲染性能
dom.subModuleId = mid;
//变成文本节点,作为子模块占位符,子模块渲染后插入到占位符前面
delete dom.tagName;
if (handle) { //需要处理
//设置props,如果改变了props,启动渲染
let o: any = {};
if (dom.props) {
for (let p of Object.keys(dom.props)) {
let v = dom.props[p];
if (p[0] === '$') { //数据
if (!o.$data) {
o.$data = {};
}
o.$data[p.substring(1)] = v;
//删除属性
delete dom.props[p];
} else {
o[p] = v;
}
}
}
//传递给模块
m.setProps(o, dom);
}
return true;
},
8
);
/**
* model指令
*/
createDirective(
'model',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
let model: Model = dom.model.$get(this.value);
if (model) {
dom.model = model;
}
return true;
},
1
);
/**
* 指令名 repeat
* 描述:重复指令
*/
createDirective(
'repeat',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
let rows = this.value;
// 无数据,不渲染
if (!Util.isArray(rows) || rows.length === 0) {
return false;
}
//索引名
const idxName = src.getProp('$index');
const parent = dom.parent;
//禁用该指令
this.disabled = true;
//避免在渲染时对src设置了model,此处需要删除
delete src.model;
for (let i = 0; i < rows.length; i++) {
if (idxName) {
rows[i][idxName] = i;
}
//渲染一次-1,所以需要+1
src.staticNum++;
let d = Renderer.renderDom(module, src, rows[i], parent, rows[i].$key + '');
//删除$index属性
if (idxName) {
delete d.props['$index'];
}
}
//启用该指令
this.disabled = false;
return false;
},
2
);
/**
* 递归指令
* 作用:在dom内部递归,用于具有相同数据结构的节点递归生成
* 递归指令不允许嵌套
* name表示递归名字,必须与内部的recur标签的ref保持一致,名字默认为default
* 典型模版
* ```
*
* ...
* ...
*
*
* ```
*/
createDirective(
'recur',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
//当前节点是递归节点存放容器
if (dom.props.hasOwnProperty('ref')) {
//如果出现在repeat中,src为单例,需要在使用前清空子节点,避免沿用上次的子节点
src.children = [];
//递归存储名
const name = '$recurs.' + (dom.props['ref'] || 'default');
let node = module.objectManager.get(name);
if (!node) {
return true;
}
let model = dom.model;
let cond = node.getDirective('recur');
let m = model[cond.value];
//不存在子层数组,不再递归
if (!m) {
return true;
}
//克隆,后续可以继续用
let node1 = node.clone();
//recur子节点不为数组,依赖子层数据,否则以来repeat数据
if (!Array.isArray(m)) {
node1.model = m;
Util.setNodeKey(node1, m.$key, true);
}
src.children = [node1];
node1.parent = src;
} else { //递归节点
let data = dom.model[this.value];
if (!data) {
return true;
}
//递归名,默认default
const name = '$recurs.' + (dom.props['name'] || 'default');
if (!module.objectManager.get(name)) {
module.objectManager.set(name, src);
}
}
return true;
},
2
);
/**
* 指令名 if
* 描述:条件指令
*/
createDirective('if',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
module.objectManager.setDomParam(dom.parent.key, '$if', this.value);
return this.value;
},
5
);
/**
* 指令名 else
* 描述:else指令
*/
createDirective(
'else',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
return module.objectManager.getDomParam(dom.parent.key, '$if') === false;
},
5
);
/**
* elseif 指令
*/
createDirective('elseif',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
let v = module.objectManager.getDomParam(dom.parent.key, '$if');
if (v === true) {
return false;
} else {
if (!this.value) {
return false;
} else {
module.objectManager.setDomParam(dom.parent.key, '$if', true);
}
}
return true;
},
5
);
/**
* elseif 指令
*/
createDirective(
'endif',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
module.objectManager.removeDomParam(dom.parent.key, '$if');
return true;
},
5
);
/**
* 指令名 show
* 描述:显示指令
*/
createDirective(
'show',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
if (this.value) {
return true;
}
return false;
},
5
);
/**
* 指令名 field
* 描述:字段指令
*/
createDirective('field',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
const type: string = dom.props['type'] || 'text';
const tgname = dom.tagName.toLowerCase();
const model = dom.model;
if (!model) {
return true;
}
let dataValue = model.$get(this.value);
if (type === 'radio') {
let value = dom.props['value'];
if (dataValue == value) {
dom.props['checked'] = 'checked';
Util.setDomAsset(dom, 'checked', true);
} else {
delete dom.props['checked'];
Util.setDomAsset(dom, 'checked', false);
}
} else if (type === 'checkbox') {
//设置状态和value
let yv = dom.props['yes-value'];
//当前值为yes-value
if (dataValue == yv) {
dom.props['value'] = yv;
Util.setDomAsset(dom, 'checked', true);
} else { //当前值为no-value
dom.props['value'] = dom.props['no-value'];
Util.setDomAsset(dom, 'checked', false);
}
} else if (tgname === 'select') { //下拉框
dom.props['value'] = dataValue;
Util.setDomAsset(dom, 'value', dataValue);
} else {
let v = (dataValue !== undefined && dataValue !== null) ? dataValue : '';
dom.props['value'] = v;
Util.setDomAsset(dom, 'value', v);
}
let event: NEvent = GlobalCache.get('$fieldChangeEvent');
if (!event) {
event = new NEvent(null, 'change',
function (model, dom) {
let el = this.getNode(dom.key);
if (!el) {
return;
}
let directive = dom.vdom.getDirective('field');
let type = dom.props['type'];
let field = directive.value;
let v = el.value;
//根据选中状态设置checkbox的value
if (type === 'checkbox') {
if (dom.props['yes-value'] == v) {
v = dom.props['no-value'];
} else {
v = dom.props['yes-value'];
}
} else if (type === 'radio') {
if (!el.checked) {
v = undefined;
}
}
//修改字段值,需要处理.运算符
let arr = field.split('.')
if (arr.length === 1) {
model[field] = v;
} else {
let temp = model;
field = arr.pop();
for (let i = 0; i < arr.length && temp; i++) {
temp = temp[arr[i]];
}
if (temp) {
temp[field] = v;
}
}
}
);
GlobalCache.set('$fieldChangeEvent', event);
}
src.addEvent(event);
return true;
},
10
);
/**
* route指令
*/
createDirective('route',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
//a标签需要设置href
if (dom.tagName.toLowerCase() === 'a') {
dom.props['href'] = 'javascript:void(0)';
}
dom.props['path'] = this.value;
//有激活属性
if (dom.props['active']) {
let acName = dom.props['active'];
delete dom.props['active'];
//active 转expression
Router.addActiveField(module, this.value, dom.model, acName);
if (this.value.startsWith(Router.currentPath) && dom.model[acName]) {
Router.go(this.value);
}
}
//添加click事件,避免重复创建事件对象,创建后缓存
let event: NEvent = GlobalCache.get('$routeClickEvent');
if (!event) {
event = new NEvent(null, 'click',
function (model, dom, evObj, e) {
let path = dom.props['path'];
if (Util.isEmpty(path)) {
return;
}
Router.go(path);
}
);
GlobalCache.set('$routeClickEvent', event);
}
src.addEvent(event);
return true;
}
);
/**
* 增加router指令
*/
createDirective('router',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
Router.routerKeyMap.set(module.id, dom.key);
return true;
}
);
/**
* 插头指令
* 用于模块中,可实现同名替换
*/
createDirective('slot',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
this.value = this.value || 'default';
let mid = dom.parent.subModuleId;
//父dom有module指令,表示为替代节点,替换子模块中的对应的slot节点;否则为子模块定义slot节点
if (mid) {
let m = ModuleFactory.get(mid);
if (m) {
//缓存当前替换节点
m.objectManager.set('$slots.' + this.value, { dom: src, model: dom.model });
//只需添加一次,用后即删除
if(src.parent){
src.parent.remove(src);
}
}
} else { //源slot节点
//获取替换节点进行替换
const cfg = module.objectManager.get('$slots.' + this.value);
if (cfg) {
//避免key重复,更新key
for (let d of cfg.dom.children) {
let model;
if (src.hasProp('innerRender')) { //内部数据渲染
model = dom.model;
}else{ //外部数据渲染
model = cfg.model;
//对象绑定到当前模块
ModelManager.bindToModule(cfg.model, module);
}
//key以s结尾,避免重复,以dom key作为附加key
Renderer.renderDom(module, d, model, dom.parent, dom.key+'s');
}
}
}
return false;
},
5
);
/**
* 指令名
* 描述:动画指令
*/
createDirective('animation',
function (module: Module, dom: IRenderedDom, src: VirtualDom) {
const confObj = this.value
if (!Util.isObject(confObj)) {
return new Error('未找到animation配置对象');
}
// 获得tigger
const tigger = confObj.tigger == false ? false : true;
// 用于判断是动画还是过渡
const type = confObj.type || "transition";
// 用于判断是否是 进入/离开动画
const isAppear = confObj.isAppear == false ? false : true;
// 提取 动画/过渡 名
const nameEnter = confObj.name?.enter || confObj.name;
const nameLeave = confObj.name?.leave || confObj.name;
// 提取 动画/过渡 持续时间
const durationEnter = confObj.duration?.enter || confObj.duration || '';
const durationLeave = confObj.duration?.leavr || confObj.duration || '';
// 提取 动画/过渡 延迟时间
const delayEnter = confObj.delay?.enter || confObj.delay || '0s';
const delayLeave = confObj.delay?.leave || confObj.delay || '0s';
// 提取 动画/过渡 时间函数
const timingFunctionEnter = confObj.timingFunction?.enter || confObj.timingFunction || 'ease';
const timingFunctionLeave = confObj.timingFunction?.leave || confObj.timingFunction || 'ease';
// 提取动画/过渡 钩子函数
const beforeEnter =
confObj.hooks?.enter?.before ? confObj.hooks.enter.before : confObj.hooks?.before || undefined;
const afterEnter =
confObj.hooks?.enter?.after ? confObj.hooks.enter.after : confObj.hooks?.after || undefined;
const beforeLeave =
confObj.hooks?.leave?.before ? confObj.hooks.leave.before : confObj.hooks?.before || undefined;
const afterLeave =
confObj.hooks?.leave?.after ? confObj.hooks.leave.after : confObj.hooks?.after || undefined;
// 定义动画或者过渡结束回调。
let handler = () => {
const el: HTMLElement = module.getNode(dom.key)
// 离开动画结束之后隐藏元素
if (!tigger) {
if (isAppear) {
// 离开动画结束之后 把元素隐藏
el.style.display = 'none';
}
if (afterLeave) {
afterLeave.apply(module.model, [module]);
}
// 这里如果style里面写了width和height 那么给他恢复成他写的,不然
[el.style.width, el.style.height] = getOriginalWidthAndHeight(dom);
// 结束之后删除掉离开动画相关的类
el.classList.remove(nameLeave + '-leave-active')
if (type == 'animation') {
el.classList.add(nameLeave + '-leave-to')
}
} else {
if (afterEnter) {
afterEnter.apply(module.model, [module]);
}
// 进入动画结束之后删除掉相关的类
el.classList.remove(nameEnter + '-enter-active')
if (type == 'animation') {
el.classList.add(nameEnter + '-enter-to')
}
}
// 清除事件监听
el.removeEventListener('animationend', handler);
el.removeEventListener('transitionend', handler);
}
// 获得真实dom
let el: HTMLElement = module.getNode(dom.key);
if (!tigger) {
// tigger为false 播放Leave动画
if (el) {
if (el.getAttribute('class').indexOf(`${nameLeave}-leave-to`) != -1) {
// 当前已经处于leave动画播放完成之后,若是进入离开动画,这时候需要他保持隐藏状态。
dom.props['class'] += ` ${nameLeave}-leave-to`;
if (isAppear) {
dom.props["style"]
? (dom.props["style"] += ";display:none;")
: (dom.props["style"] = "display:none;");
}
return true;
}
// // 确保在触发动画之前还是隐藏状态
// 调用函数触发 Leave动画/过渡
changeStateFromShowToHide(el);
return true;
} else {
// el不存在,第一次渲染
if (isAppear) {
// 是进入离开动画,管理初次渲染的状态,让他隐藏
dom.props["style"]
? (dom.props["style"] += ";display:none;")
: (dom.props["style"] = "display:none;");
}
// 下一帧
setTimeout(() => {
// el已经渲染出来,取得el 根据动画/过渡的类型来做不同的事
let el: HTMLElement = module.getNode(dom.key)
if (isAppear) {
// 动画/过渡 是进入离开动画/过渡,并且当前是需要让他隐藏所以我们不播放动画,直接隐藏。
el.classList.add(`${nameLeave}-leave-to`)
// 这里必须将这个属性加入到dom中,否则该模块其他数据变化触发增量渲染时,diff会将这个节点重新渲染,导致显示异常
// 这里添加添加属性是为了避免diff算法重新渲染该节点
dom.props['class'] += ` ${nameLeave}-leave-to`;
el.style.display = 'none'
} else {
// 动画/过渡 是 **非进入离开动画/过渡** 我们不管理元素的隐藏,所以我们让他播放一次Leave动画。
changeStateFromShowToHide(el);
}
}, 0);
}
// 通过虚拟dom将元素渲染出来
return true
} else {
// tigger为true 播放Enter动画
if (el) {
if (el.getAttribute('class').indexOf(`${nameEnter}-enter-to`) != -1) {
// 这里不需要像tigger=false时那样处理,这时候他已经处于进入动画播放完毕状态,
// 模块内其他数据变化引起该指令重新执行,这时候需要他保持现在显示的状态,直接返回true
dom.props['class'] += ` ${nameEnter}-enter-to`;
return true;
}
if (isAppear) {
// 如果是进入离开动画,在播放enter动画之前确保该元素是隐藏状态
// 确保就算diff更新了该dom还是有隐藏属性
dom.props["style"]
? (dom.props["style"] += ";display:none;")
: (dom.props["style"] = "display:none;");
}
// 调用函数触发Enter动画/过渡
changeStateFromHideToShow(el);
} else {
// el不存在,是初次渲染
if (isAppear) {
// 管理初次渲染元素的隐藏显示状态
dom.props["style"]
? (dom.props["style"] += ";display:none;")
: (dom.props["style"] = "display:none;");
}
// 下一帧
setTimeout(() => {
// 等虚拟dom把元素更新上去了之后,取得元素
let el: HTMLElement = module.getNode(dom.key)
if (isAppear) {
// 这里必须将这个属性加入到dom中,否则该模块其他数据变化触发增量渲染时,diff会将这个节点重新渲染,导致显示异常
// 这里添加添加属性是为了避免diff算法重新渲染该节点
dom.props['class'] += ` ${nameEnter}-enter-to`;
el.style.display = 'none';
}
// Enter动画与Leave动画不同,
// 不管动画是不是进入离开动画,在初次渲染的时候都要执行一遍动画
// Leave动画不一样,如果是开始离开动画,并且初次渲染的时候需要隐藏,那么我们没有必要播放一遍离开动画
changeStateFromHideToShow(el);
}, 0);
}
// 通过虚拟dom将元素渲染出来
return true
}
/**
* 播放Leave动画
* @param el 真实dom
*/
function changeStateFromShowToHide(el: HTMLElement) {
// 动画类型是transitiojn
if (type == 'transition') {
// 前面已经对transition的初始状态进行了设置,我们需要在下一帧设置结束状态才能触发过渡
// 获得宽高的值 因为 宽高的auto 百分比 calc计算值都无法拿来触发动画或者过渡。
let [width, height] = getElRealSzie(el);
// setTimeout(() => {
requestAnimationFrame(() => {
// 移除掉上一次过渡的最终状态
el.classList.remove(nameEnter + '-enter-to')
// 设置过渡的类名
el.classList.add(nameLeave + '-leave-active');
// 设置离开过渡的开始类
el.classList.add(nameLeave + '-leave-from');
// fold过渡的开始状态
if (nameLeave == 'fold-height') {
el.style.height = height;
} else if (nameLeave == 'fold-width') {
el.style.width = width;
}
// 处理离开过渡的延时
el.style.transitionDelay = delayEnter;
// 处理过渡的持续时间
if (durationEnter != '') {
el.style.transitionDuration = durationEnter;
}
// 处理过渡的时间函数
if (timingFunctionEnter != 'ease') {
el.style.transitionTimingFunction = timingFunctionEnter;
}
// 在触发过渡之前执行hook
if (beforeLeave) {
beforeLeave.apply(module.model, [module]);
}
requestAnimationFrame(() => {
// 添加结束状态
el.classList.add(nameLeave + '-leave-to');
// 在动画或者过渡开始之前移除掉初始状态
el.classList.remove(nameLeave + '-leave-from');
if (nameLeave == 'fold-height') {
el.style.height = '0px';
} else if (nameLeave == 'fold-width') {
el.style.width = '0px';
}
// 添加过渡结束事件监听
el.addEventListener('transitionend', handler);
})
});
} else {
requestAnimationFrame(() => {
// 动画类型是aniamtion
el.classList.remove(nameEnter + '-enter-to');
// 动画延时时间
el.style.animationDelay = delayLeave;
// 动画持续时间
if (durationLeave != '') {
el.style.animationDuration = durationLeave;
}
if (timingFunctionLeave != 'ease') {
el.style.animationTimingFunction = timingFunctionLeave;
}
// 在触发动画之前执行hook
if (beforeLeave) {
beforeLeave.apply(module.model, [module]);
}
// 触发一次回流reflow
void el.offsetWidth;
// 添加动画类名
el.classList.add(nameLeave + '-leave-active');
//添加动画结束时间监听
el.addEventListener('animationend', handler);
// })
})
}
}
/**
* 播放Enter动画
* @param el 真实dom
*/
function changeStateFromHideToShow(el: HTMLElement) {
// 动画类型是transition
if (type == 'transition') {
// 对于进入/离开动画
// Enter过渡的延迟时间与Leave过渡的延迟时间处理不一样
// 我们这里把延迟统一设置成0s,然后通过定时器来设置延时,
// 这样可以避免先渲染一片空白区域占位,然后再延时一段时间执行过渡效果。
el.style.transitionDelay = '0s';
let delay = parseFloat(delayEnter) * 1000;
setTimeout(() => {
let [width, height] = getElRealSzie(el);
// 在第一帧设置初始状态
// 移除掉上一次过渡的最终状态
el.classList.remove(nameLeave + '-leave-to');
// 添加过渡的类名
el.classList.add(nameEnter + '-enter-active');
// 给进入过渡设置开始类名
el.classList.add(nameEnter + '-enter-from');
// 获得元素的真实尺寸
if (nameEnter == 'fold-height') {
el.style.height = '0px'
} else if (nameEnter == 'fold-width') {
el.style.width = '0px'
}
// 设置过渡持续时间
if (durationEnter != '') {
el.style.transitionDuration = durationEnter;
}
// 设置过渡时间函数
if (timingFunctionEnter != 'ease') {
el.style.transitionTimingFunction = timingFunctionEnter;
}
// 第二帧将带有初始状态的元素显示出来,如果不开这一帧那么fade的进入过渡在初次渲染的时候会被当作离开过渡触发。
requestAnimationFrame(() => {
// 下一帧请求过渡效果
// 过渡开始之前先将元素显示
if (isAppear) {
el.style.display = '';
}
// 第三帧触发过渡
requestAnimationFrame(() => {
if (beforeEnter) {
beforeEnter.apply(module.model, [module]);
}
// 增加 过渡 结束类名
el.classList.add(nameEnter + '-enter-to');
// 移除过渡的开始类名
el.classList.remove(nameEnter + '-enter-from');
if (nameEnter == 'fold-height') {
el.style.height = height;
} else if (nameEnter == 'fold-width') {
el.style.width = width;
}
el.addEventListener('transitionend', handler);
})
})
}, delay);
} else {
// 动画类型是aniamtion
// 这里动画的延迟时间也与过渡类似的处理方式。
el.style.animationDelay = "0s";
let delay = parseFloat(delayEnter) * 1000;
setTimeout(() => {
// 动画开始之前先将元素显示
requestAnimationFrame(() => {
el.classList.remove(nameLeave + '-leave-to');
// 设置动画的持续时间
if (durationEnter != '') {
el.style.animationDuration = durationEnter;
}
// 设置动画的时间函数
if (timingFunctionEnter != 'ease') {
el.style.animationTimingFunction = durationEnter;
}
if (isAppear) {
el.style.display = '';
}
// 在触发过渡之前执行hook
if (beforeEnter) {
beforeEnter.apply(module.model, [module]);
}
// 触发一次回流reflow
void el.offsetWidth;
// 重新添加类名
el.classList.add(nameEnter + '-enter-active');
el.addEventListener('animationend', handler);
})
}, delay);
}
}
/**
* 获取真实dom绘制出来之后的宽高
* @param el 真实dom
* @returns 真实dom绘制出来之后的宽高
*/
function getElRealSzie(el: HTMLElement) {
if (el.style.display == 'none') {
// 获取原先的
const position = window.getComputedStyle(el).getPropertyValue("position")
const vis = window.getComputedStyle(el).getPropertyValue("visibility")
// 先脱流
el.style.position = 'absolute';
// 然后将元素变为
el.style.visibility = 'hidden';
el.style.display = '';
let width = window.getComputedStyle(el).getPropertyValue("width");
let height = window.getComputedStyle(el).getPropertyValue("height");
// 还原样式
el.style.position = position;
el.style.visibility = vis;
el.style.display = 'none';
return [width, height]
} else {
let width = window.getComputedStyle(el).getPropertyValue("width");
let height = window.getComputedStyle(el).getPropertyValue("height");
return [width, height]
}
}
/**
* 如果dom上得style里面有width/height
* @param dom 虚拟dom
* @returns 获得模板上的width/height 如果没有则返回空字符串
*/
function getOriginalWidthAndHeight(dom: IRenderedDom): Array {
const oStyle = dom.vdom.getProp('style');
let width: string;
let height: string;
if (oStyle) {
let arr = oStyle.trim().split(/;\s*/);
for (const a of arr) {
if (a.startsWith('width')) {
width = a.split(":")[1];
}
if (a.startsWith('height')) {
height = a.split(':')[1];
}
}
}
width = width || '';
height = height || '';
return [width, height];
}
},
9
);
}());