/***
* Created by lzq on 2018/11/02.
* CtMobile.js
* CtMobie移动端开发框架(依赖jQuery)
* WEB主体形支持三种model模式
* mode1: 本地锚点模式(inline模式)
* 模板都在本地的一个html中进行预先定义
* mode2: Ajax加载模式
* 只有第一页的模板在html进行预先定义,其他页面的模板用Ajax动态加载
* mode3: 混合模式
* mode1和mode2混合进行
*/
import $ from "jquery";
import Page from "./Page";
import Router from "./Router";
import BorasdCast from "./BorasdCast";
/**
* 根据模板pageId获取模板的DOM对象
* @access private
* @param {string} pageId
* @return {HtmlElement}
*/
function getTemplateDOMById(pageId) {
if (pageId.indexOf("?") !== -1) {
pageId = pageId.substring(0, pageId.indexOf("?"));
}
return $(this.templateDB[pageId])[0];
}
/**
* 获取模板的属性
* @access private
* @param {string} pageId
* @param {string} attr
* @return {Object}
*/
function getTemplateConfig(pageId, attr) {
const dom = getTemplateDOMById.call(this, pageId);
let config = dom.getAttribute(attr);
if ((attr === "ct-data-mode") && !config) {
config = "standard";
}
return config;
}
/**
* 页面载入完成后,支持Promise
* @access private
* @callback
* @return {Promise}
*/
function readyPromise() {
return new Promise((resolve) => {
$(window.document).ready(() => {
resolve();
});
});
}
/**
* DOM载入完成后,支持Promise
* @access private
* @callback
* @return {Promise}
*/
function DOMContentLoadedPromise() {
return new Promise((resolve) => {
window.addEventListener("DOMContentLoaded", () => {
resolve();
});
});
}
/**
* cordova的设备载入完成后的回调函数,支持Promise
* @access private
* @callback
* @return {Promise}
*/
function devicereadyPromise() {
return new Promise((resolve) => {
window.document.addEventListener("deviceready", () => {
resolve();
});
});
}
/**
* 触发自定义的Html事件
* @access private
* @param {HtmlElement} dom - 触发的HTML对象
* @param {string} type - 触发的事件
* @param {Array} params - 参数
*/
function fireEvent(dom, type, params = []) {
$(dom).trigger(type, params);
}
/**
* 预加载Ajax的页面
* @access private
* @callback
* @param {HtmlElement} pageDOM
* @return {Promise}
*/
function AjaxPreloadPromise(pageDOM) {
const self = this;
return new Promise((resolve, reject) => {
let asyncTasks = [];
console.time("预加载Ajax用时");
/***
* 查看是否有需要预加载的Ajax页面
*/
$(pageDOM).find("a[ct-data-preload=true][ct-pageId]").each((index, aDom) => {
const ctDataAjax = aDom.getAttribute("ct-data-ajax");
const pageId = aDom.getAttribute("ct-pageId");
if (
!pageId &&
ctDataAjax &&
ctDataAjax === "false"
) {
return false;
}
const pageRouterConfig = self.config.router[pageId];
const href = (pageRouterConfig && pageRouterConfig.url) ? pageRouterConfig.url : "";
if (!href) {
return false;
}
asyncTasks.push($.ajax({
dataType: "text",
url: href,
success: (templateText) => {
this.templateDB[pageId] = CtMobileFactory.getPageTemplateStrByAjaxStr(templateText);
},
error: (error, status, thrown) => {
reject(error);
}
}));
});
if (asyncTasks.length !== 0) {
Promise.all(asyncTasks).then(() => {
console.timeEnd("预加载Ajax用时");
resolve();
}).catch(() => {
reject();
});
} else {
resolve();
}
});
}
/**
* 初始化本地模板
* 模板是框架的基础,一切都是基于模板生成的
* 将<div ct-data-role="page"></div>的元素缓存到js对象中
* {
* ct-data-role的值(id):outerHTML为值
* }
* @access private
*/
function initialLocalTemplatePromise() {
const self = this;
return new Promise((resolve, reject) => {
console.time("初始化本地模板用时:");
/***
* 遍历所有含有ct-data-role="page"的元素并且删除
*/
let ajaxPreloadPromiseTasks = [];
$(this.bodyDOM).find("div[ct-data-role='page']").each(function () {
self.templateDB[this.getAttribute("id")] = this.outerHTML;
/***
* Ajax预处理
*/
ajaxPreloadPromiseTasks.push(AjaxPreloadPromise.call(self, this));
this.parentNode.removeChild(this);
});
Promise.all(ajaxPreloadPromiseTasks).then(() => {
console.timeEnd("初始化本地模板用时:");
resolve();
}).catch((error) => {
reject(error);
});
});
}
/**
* 预加载pageDom中所有要预加载的页面
* @access private
* @param {HtmlElement} pageDom
* @return {Promise}
*/
function preload(pageDom) {
const self = this;
return new Promise((resolve, reject) => {
let ajaxPreloadPromiseTasks = [];
/**
* Ajax预处理
*/
ajaxPreloadPromiseTasks.push(AjaxPreloadPromise.call(self, pageDom));
Promise.all(ajaxPreloadPromiseTasks).then(() => {
resolve();
}).catch((error) => {
reject(error);
});
});
}
/**
* 捕获a标签的事件
* @access private
* @callback
* @return {Promise}
*/
function LinkCapturePromise() {
const self = this;
return new Promise((resolve) => {
console.time("捕获a标签用时:");
/***
* 初始化 link-capture events
*/
window.document.addEventListener("click", function (e) {
e.preventDefault();
let target = e.target;
if (!target) return;
// 不是a元素
if (
target.tagName.toLocaleLowerCase() !== "a" &&
!(target = CtMobileFactory.getParentElementByTag(target, "a"))
) {
return;
}
const ctDataAjax = target.getAttribute("ct-data-ajax");
// 如果用户不想让框架控制a元素
if (
ctDataAjax &&
ctDataAjax === "false"
) return;
const ctPageId = target.getAttribute("ct-pageId");
const ctParameter = target.getAttribute("ct-parameter") || "";
if (!ctPageId) {
return;
}
const pageRouterConfig = self.config.router[ctPageId];
const href = `${(pageRouterConfig && pageRouterConfig.url) ? pageRouterConfig.url : "#" + ctPageId}?pageId=${ctPageId}${ctParameter}`;
self.startPage(href, {
reload: target.getAttribute("ct-reload") === "true" ? true : self.config.linkCaptureReload
});
return false;
});
console.timeEnd("捕获a标签用时:");
resolve();
});
}
/**
* 创建实例
* @access private
* @param {Function} Class
* @param {string} id
* @param {string} pageId
* @return {Page}
*/
function newInstance({Class, id, pageId}) {
const typeofName = (typeof Class).toLowerCase();
const ctDataMode = getTemplateConfig.call(this, pageId, "ct-data-mode");
let Constructor;
/***
* 如果Class可以实例化
*/
if (typeofName === "function") {
Constructor = Class;
}
/***
* 如果Class不可以实例化则用缺省的Page进行实例化
*/
else if (typeofName === "undefined") {
Constructor = Page;
}
if (!Constructor) {
throw "页面管理类无法初始化";
return false;
}
/***
* 如果是singleInstance 或 singleInstanceResult
*/
if (ctDataMode.toLowerCase().indexOf("singleinstance") !== -1) {
if (!this.getSingleInstance(pageId)) {
this.singleInstances[pageId] = new Constructor(this, id);
}
return this.singleInstances[pageId];
} else {
return new Constructor(this, id);
}
}
/**
* 通过ID创建Page对象
* @access private
* @param {string} id
* @return {Promise}
*/
function createPage(id) {
return new Promise((resolve, reject) => {
const pageId = id.substring(0, id.lastIndexOf("_"));
let Class;
const pageRouterConfig = this.config.router[pageId];
if (pageRouterConfig) {
const component = this.config.router[pageId].component;
if (component && component.then) {
component.then((Page) => {
if (Page) {
Class = Page.default;
// 可以进行实例化,一般都是inline模式
if (Class) {
resolve(newInstance.call(this, {Class, id, pageId}));
} else {
reject();
}
} else {
reject();
}
}).catch((error) => {
reject(error);
});
} else {
// 使用缺省的Page实例
resolve(newInstance.call(this, {Page, id, pageId}));
//throw `没有${pageId}的页面`;
}
} else {
resolve(newInstance.call(this, {Page, id, pageId}));
}
});
}
/**
* 页面载入完成的回调函数
* @access private
* @callback
*/
function onReady() {
const self = this;
/***
* 判断页面是否已经ready
*/
if (this.hasInited) return;
this.hasInited = true;
/***
* window.document.body的jQuery
*/
this.bodyDOM = window.document.body;
/***
* 存放完全单例对象的容器
*/
this.singleInstances = null;
/***
* 创建page切换时的遮罩层
*/
this.maskDOM = $(
"<div class='ct-page-mask'>" +
" <div opt='animation' class='la-ball-circus la-dark' style='color:#3e98f0;'>" +
" <div></div>" +
" <div></div>" +
" <div></div>" +
" <div></div>" +
" <div></div>" +
" </div>" +
"</div>")[0];
this.bodyDOM.appendChild(this.maskDOM);
/***
* initialLocalTemplate都完成后初始化第一页 and LinkCapture
*/
Promise.all(
[
initialLocalTemplatePromise.call(this),
LinkCapturePromise.call(this)
]
).then(() => {
console.timeEnd("总用时");
fireEvent(window.document, "pageBeforeChange", [CtMobileFactory.getUrlParam(window.location.hash)]);
/***
* 初始化第一页
* TODO:初始化第一页
*/
createPage.call(this, this.getFirstId()).then((page) => {
page.start(0, () => {
/***
* if(有hash值) 加载的不是首页而是某一个指定的页面 {
* 调用startPage即可
* startPage需要三部分值
* 1.html的路径
* 2.pageId
* 3.parameter
* }
*/
const hash = window.location.hash;
if (!hash) return false;
const pageId = self.getPageIdByHash();
if (!pageId) return false;
const pageRouterConfig = self.config.router[pageId];
if ((pageRouterConfig && !pageRouterConfig.url)) return;
const url = (pageRouterConfig && pageRouterConfig.url) ? pageRouterConfig.url : `#${pageId}`;
const parameter = self.getParameterByHash();
const searchObj = CtMobileFactory.getUrlParam(`${url}${parameter}`);
self.startPage(`${url}${parameter}${parameter ? (searchObj.pageId ? '' : `&pageId=${pageId}`) : `?pageId=${pageId}`}`, {
reload: self.config.linkCaptureReload
});
}
);
});
});
}
/**
* initCtMobile
* @access private
*/
function init() {
const {supportCordova = false} = this.config;
onReady = onReady.bind(this);
/***
* 如果开启了对cordova的支持
*/
if (supportCordova) {
/***
* 如果开启了对cordova的支持,那么页面完成事件和cordova的deviceReady事件必须同时完成后才能支持后续代码
*/
Promise.all([
readyPromise(),
DOMContentLoadedPromise(),
devicereadyPromise()
]).then(() => {
onReady();
fireEvent(window.document, "DOMContentAndDeviceReady");
}).catch((error) => {
});
}
else {
/***
* 页面载入完成事件
*/
$(window.document).ready(onReady);
/***
* 自动 init
*/
window.addEventListener("DOMContentLoaded", onReady);
}
}
/**
* CtMobile
* @class CtMobile
* @classdesc 管理所有的行为
*/
class CtMobile {
/**
* @constructor
* @param {Object} config -
* config {
* supportCordova: [true | false],是否支持cordova,默认为false
* linkCaptureReload: [true | false],<a>标签加载页面是否改变浏览器历史,默认为true
* router: Object {
* id (ct-data-role="page"的id属性): Object{
* url:String (页面的地址)
* component: Function (返回一个Prmise)
* }
* }
* }
*/
constructor(config) {
this.config = config;
// 是否初始化过
this.hasInited = false;
// 页面的模板数据
this.templateDB = {};
// page的zIndex
this.zIndex = 0;
// 路由对象
this.router = new Router(this);
// 广播对象
this.borasdcast = new BorasdCast();
init.call(this);
}
/**
* 页面跳转
* @param {string} pageId - (pageId = pageId + params) 如: page1?a=1&b=2;
* @param {Object} option {
* reload : [true | false]
* }
*/
startPage(href, option) {
this.router.startPage(href, option);
}
/**
* 通过ID创建Page对象
* @param {string} id
* @return {Page}
*/
createPage(id) {
return createPage.call(this, id);
}
/**
* 预加载pageDom中所有要预加载的页面
* @param {HtmlElement} pageDom
* @return {Promise}
*/
preload(pageDom) {
return preload.call(this, pageDom);
}
/**
* 返回
*/
back() {
this.router.go(-1);
}
/**
* 获取第一页真正的ID
* @return {string}
*/
getFirstId() {
return this.getId(this.getFirstPageId());
}
/**
* 根据模板page的ID获取真正page的ID
* 注释:pageId_时间戳?parameters
* @param {string} pageId
* @return {string}
*/
getId(pageId) {
let id = "";
const index = pageId.indexOf("?");
if (index !== -1) {
id = pageId.substring(0, index) + "_" + new Date().getTime() + pageId.substring(index);
} else {
id = pageId + "_" + new Date().getTime();
}
return id;
}
/**
* 通过hash值获取pageId
* 例子: "#info_1541214530597?id=1"
* @return {string}
*/
getPageIdByHash() {
let hash = window.location.hash;
if (!hash) return "";
if (hash.indexOf("?") !== -1) {
hash = hash.substring(0, hash.lastIndexOf("?"));
return hash.substring(1, hash.lastIndexOf("_"));
} else {
return hash.substring(1, hash.lastIndexOf("_"));
}
}
/**
* 通过hash获取参数Parameter
* @return {string}
*/
getParameterByHash() {
let hash = window.location.hash;
if (!hash) return "";
if (hash.indexOf("?") !== -1) {
return hash.substring(hash.lastIndexOf("?"));
} else {
return "";
}
}
/**
* 获取第一个页面的pageId
* @return {string}
*/
getFirstPageId() {
let id;
for (let p in this.templateDB) {
id = p;
break;
}
return id;
}
/**
* 根据ID获取page对象
* @param {string} id
* @return {Page}
*/
getPageById(id) {
return this.router.getPageById(this.indexOfById(id));
}
/**
* 根据pageId获取单例对象
* @param {string} pageId
* @return {Page}
*/
getSingleInstance(pageId) {
if (!this.singleInstances) {
this.singleInstances = {};
}
return this.singleInstances[pageId];
}
/**
* 触发一个自定义事件
* @param {HtmlElement} dom
* @param {string} type
* @param {Object} params
*/
fireEvent(dom, type, params) {
fireEvent(dom, type, params);
}
/**
* 获取模板的属性
* @param {string} pageId
* @param {string} attr
* @return {Object}
*/
getTemplateConfig(pageId, attr) {
return getTemplateConfig.call(this, pageId, attr);
}
/**
* 根据索引获取page对象
* @param {string} index
* @returns {Page}
*/
getPageByIndex(index) {
return this.router.getPageByIndex(index);
}
/**
* 根据id获取索引
* @param {string} id
* @returns {number}
*/
indexOfById(id) {
let index = -1;
for (let i = 0, len = this.getHistoryLength(); i < len; i++) {
if (this.getPageByIndex(i).getId() === id) {
index = i;
break;
}
}
return index;
}
/**
* 获取历史记录中的栈第一个元素
* @return {Page}
*/
getFirstPage() {
return this.router.getPageByIndex(0);
}
/**
* 获取历史记录中的栈顶的元素
* @returns {Page}
*/
getLastPage() {
return this.router.getLastPage();
}
/**
* 获取转场的参数
* @return {Object}
*/
getParameter() {
return this.router.getParameter();//$.extend({}, _parameter);
}
/**
* 获取历史栈长度
* @return {number}
*/
getHistoryLength() {
return this.router.getHistoryLength();
}
/**
* 获取父窗体的setRequest的值
* @param {Page} page
* @return {Object}
*/
getRequest(page) {
if (this.getHistoryLength() === 0 || this.getHistoryLength() === 1) {
return {};
} else {
const index = this.indexOfById(page.getId());
if (index <= 0 || index > this.getHistoryLength() - 1) {
return {};
} else {
return this.getPageByIndex(this.getHistoryLength() - 2).requestIntent || {};
}
}
}
/**
* 注册Receiver对象
* @params {Object} intentFilter -
* {
* el: HtmlElement
* action:[string] action
* priority:[number] 优先级
* categorys:[array] 分类
* }
* @params {Function} handler - receiver执行的handler
*/
registerReceiver(intentFilter, handler) {
this.borasdcast.registerReceiver(intentFilter, handler);
}
/**
* 执行Receiver通过Id
* @param {string} id
* @param {string} jsonStr
*/
executeReceiverById(id, jsonStr) {
this.borasdcast.executeReceiverById(id, jsonStr);
}
/**
* 解除注册Receiver对象
* @params {Function} handler
*/
unregisterReceiver(action, handler) {
this.borasdcast.unregisterReceiver(action, handler);
}
/**
* 解除注册通过Page中的Dom
* @param {HtmlElement} el
*/
unregisterReceiverByDom(el) {
this.borasdcast.unregisterReceiverByDom(el);
}
/**
* 发送无序广播
* @param {Object} intent -
* {
* action:[string] action
* categorys:[array] 分类
* bundle:Object 参数
* }
*/
sendBroadcast(intent) {
this.borasdcast.sendBroadcast(intent);
}
/**
* 发送有序广播
* @param {Object} intent -
* {
* action:[string] action
* categorys:[array] 分类
* bundle:Object 参数
* }
*/
sendOrderedBroadcast(intent) {
this.borasdcast.sendOrderedBroadcast(intent);
}
}
/**
* CtMobileFactory
* @class
*/
const CtMobileFactory = {
/**
* 将转场参数转换为对象
* @param {string} url
* @return {Object}
*/
getUrlParam(url) {
const reg_url = /^[^\?]+\?([\w\W]+)$/,
reg_para = /([^&=]+)=([\w\W]*?)(&|$)/g,
arr_url = reg_url.exec(url),
ret = {};
if (arr_url && arr_url[1]) {
const str_para = arr_url[1];
let result;
while ((result = reg_para.exec(str_para)) != null) {
ret[result[1]] = decodeURI(result[2]);
}
}
return ret;
},
/**
* getParentElementByTag
* @param {HtmlElement} el
* @param {string} tag
* @return {HtmlElement}
*/
getParentElementByTag(el, tag) {
if (!tag) return null;
let element = null, parent = el;
let popup = function () {
parent = parent.parentElement;
if (!parent) return null;
const tagParent = parent.tagName.toLocaleLowerCase();
if (tagParent === tag) {
element = parent;
} else if (tagParent === "body") {
element = null;
} else {
popup();
}
};
popup();
return element;
},
/**
* 根据Ajax返回值获取TemplateStr
* @param {string} templateText
* @return {string}
*/
getPageTemplateStrByAjaxStr(templateText) {
let pageDom;
templateText = templateText.trim();
const pageElemRegex = new RegExp("(<[^>]+\\bct-data-role=[\"']?page[\"']?[^>]*>)");
if (pageElemRegex.test(templateText)) {
const strArr = templateText.split(/<\/?body[^>]*>/gmi);
pageDom = $(((strArr || []).length === 0 ? "" : strArr.length > 1 ? strArr[1] : strArr[0]) || "")[0];
}
if (pageDom) {
return pageDom.outerHTML;
} else {
return "";
}
},
/**
* 创建CtMobile
* @param {object} config
* @return {CtMobile}
*/
create(config) {
return new CtMobile(config);
}
};
export default CtMobileFactory;