gulp-bird
Version:
bird-v2
549 lines (507 loc) • 16.2 kB
JavaScript
/**
* vConsole core class
*
* @author WechatFE
*/
import pkg from '../../package.json';
import * as tool from '../lib/tool.js';
import $ from '../lib/query.js';
import './core.scss';
import tpl from './core.html';
import tplTabbar from './tabbar.html';
import tplTabbox from './tabbox.html';
import tplTopBarItem from './topbar_item.html';
import tplToolItem from './tool_item.html';
class VConsole {
constructor() {
let that = this;
this.version = pkg.version;
this.html = tpl;
this.$dom = null;
this.activedTab = '';
this.tabList = [];
this.pluginList = {};
this.isReady = false;
this.switchPos = {
x: 10, // right
y: 10, // bottom
startX: 0,
startY: 0,
endX: 0,
endY: 0
};
// export helper functions to public
this.tool = tool;
this.$ = $;
let _onload = function () {
that._render();
that._mockTap();
that._bindEvent();
that._autoRun();
that._setFontSize();
};
if (document !== undefined) {
if (document.readyState == 'complete') {
_onload();
} else {
$.bind(window, 'load', _onload);
}
} else {
// if document does not exist, wait for it
let _timer;
let _pollingDocument = function () {
if (document && document.readyState == 'complete') {
_timer && clearTimeout(_timer);
_onload();
} else {
_timer = setTimeout(_pollingDocument, 1);
}
};
_timer = setTimeout(_pollingDocument, 1);
}
}
/**
* render panel DOM
* @private
*/
_render() {
let id = '#__vconsole';
if (!$.one(id)) {
let e = document.createElement('div');
e.innerHTML = this.html;
const birds = document.querySelector('.bird-tools__menu');
birds.insertAdjacentElement('afterbegin', e.children[0]);
}
this.$dom = $.one(id);
// reposition switch button
let $switch = $.one('.vc-switch', this.$dom);
let switchX = tool.getStorage('switch_x') * 1,
switchY = tool.getStorage('switch_y') * 1;
if (switchX || switchY) {
// check edge
if (switchX + $switch.offsetWidth > document.documentElement.offsetWidth) {
switchX = document.documentElement.offsetWidth - $switch.offsetWidth;
}
if (switchY + $switch.offsetHeight > document.documentElement.offsetHeight) {
switchY = document.documentElement.offsetHeight - $switch.offsetHeight;
}
if (switchX < 0) { switchX = 0; }
if (switchY < 0) { switchY = 0; }
this.switchPos.x = switchX;
this.switchPos.y = switchY;
$.one('.vc-switch').style.right = switchX + 'px';
$.one('.vc-switch').style.bottom = switchY + 'px';
}
// remove from less to present transition effect
$.one('.vc-mask', this.$dom).style.display = 'none';
};
/**
* simulate tap event by touchstart & touchend
* @private
*/
_mockTap() {
let tapTime = 700, // maximun tap interval
tapBoundary = 10; // max tap move distance
let lastTouchStartTime,
touchstartX,
touchstartY,
touchHasMoved = false,
targetElem = null;
this.$dom.addEventListener('touchstart', function (e) { // todo: if double click
if (lastTouchStartTime === undefined) {
let touch = e.targetTouches[0];
touchstartX = touch.pageX;
touchstartY = touch.pageY;
lastTouchStartTime = e.timeStamp;
targetElem = (e.target.nodeType === Node.TEXT_NODE ? e.target.parentNode : e.target);
}
}, false);
this.$dom.addEventListener('touchmove', function (e) {
let touch = e.changedTouches[0];
if (Math.abs(touch.pageX - touchstartX) > tapBoundary || Math.abs(touch.pageY - touchstartY) > tapBoundary) {
touchHasMoved = true;
}
});
this.$dom.addEventListener('touchend', function (e) {
// move and time within limits, manually trigger `click` event
if (touchHasMoved === false && e.timeStamp - lastTouchStartTime < tapTime && targetElem != null) {
let tagName = targetElem.tagName.toLowerCase(),
needFocus = false;
switch (tagName) {
case 'textarea': // focus
needFocus = true; break;
case 'input':
switch (targetElem.type) {
case 'button':
case 'checkbox':
case 'file':
case 'image':
case 'radio':
case 'submit':
needFocus = false; break;
default:
needFocus = !targetElem.disabled && !targetElem.readOnly;
}
default:
break;
}
if (needFocus) {
targetElem.focus();
} else {
e.preventDefault(); // prevent click 300ms later
}
let touch = e.changedTouches[0];
let event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
event.forwardedTouchEvent = true;
event.initEvent('click', true, true);
targetElem.dispatchEvent(event);
}
// reset values
lastTouchStartTime = undefined;
touchHasMoved = false;
targetElem = null;
}, false);
}
/**
* bind DOM events
* @private
*/
_bindEvent() {
let that = this;
// show console panel
$.bind($.one('.vc-switch', that.$dom), 'click', function () {
that.show();
});
// hide console panel
$.bind($.one('.vc-hide', that.$dom), 'click', function () {
that.hide();
});
// hide console panel when tap background mask
$.bind($.one('.vc-mask', that.$dom), 'click', function (e) {
if (e.target != $.one('.vc-mask')) {
return false;
}
that.hide();
});
// show tab box
$.delegate($.one('.vc-tabbar', that.$dom), 'click', '.vc-tab', function (e) {
let tabName = this.dataset.tab;
if (tabName == that.activedTab) {
return;
}
that.showTab(tabName);
});
// after console panel, trigger a transitionend event to make panel's property 'display' change from 'block' to 'none'
$.bind($.one('.vc-panel', that.$dom), 'transitionend webkitTransitionEnd oTransitionEnd otransitionend', function (e) {
if (e.target != $.one('.vc-panel')) {
return false;
}
if (!$.hasClass(that.$dom, 'vc-toggle')) {
e.target.style.display = 'none';
}
});
// disable background scrolling
let $content = $.one('.vc-content', that.$dom);
let preventMove = false;
$.bind($content, 'touchstart', function (e) {
let top = $content.scrollTop,
totalScroll = $content.scrollHeight,
currentScroll = top + $content.offsetHeight;
if (top === 0) {
// when content is on the top,
// reset scrollTop to lower position to prevent iOS apply scroll action to background
$content.scrollTop = 1;
// however, when content's height is less than its container's height,
// scrollTop always equals to 0 (it is always on the top),
// so we need to prevent scroll event manually
if ($content.scrollTop === 0) {
if (!$.hasClass(e.target, 'vc-cmd-input')) { // skip input
preventMove = true;
}
}
} else if (currentScroll === totalScroll) {
// when content is on the bottom,
// do similar processing
$content.scrollTop = top - 1;
if ($content.scrollTop === top) {
if (!$.hasClass(e.target, 'vc-cmd-input')) {
preventMove = true;
}
}
}
});
$.bind($content, 'touchmove', function (e) {
if (preventMove) {
e.preventDefault();
}
});
$.bind($content, 'touchend', function (e) {
preventMove = false;
});
};
/**
* auto run after initialization
* @private
*/
_autoRun() {
this.isReady = true;
// init plugins
for (let id in this.pluginList) {
this._initPlugin(this.pluginList[id]);
}
// show first tab
if (this.tabList.length > 0) {
this.showTab(this.tabList[0]);
}
}
_setFontSize() {
// 设置文本大小,为 em 适配做准备
var docEl = document.documentElement;
var rem = docEl.clientWidth / 10;
var vconsole = document.getElementById('__vconsole');
vconsole.style.fontSize = rem + 'px'
}
/**
* init a plugin
* @private
*/
_initPlugin(plugin) {
let that = this;
// start init
plugin.trigger('init');
// render tab (if it is a tab plugin then it should has tab-related events)
plugin.trigger('renderTab', function (tabboxHTML) {
// add to tabList
that.tabList.push(plugin.id);
// render tabbar
let $tabbar = $.render(tplTabbar, { id: plugin.id, name: plugin.name });
$.one('.vc-tabbar', that.$dom).insertAdjacentElement('beforeend', $tabbar);
// render tabbox
let $tabbox = $.render(tplTabbox, { id: plugin.id });
if (!!tabboxHTML) {
if (tool.isString(tabboxHTML)) {
$tabbox.innerHTML += tabboxHTML;
} else if (tool.isFunction(tabboxHTML.appendTo)) {
tabboxHTML.appendTo($tabbox);
} else if (tool.isElement(tabboxHTML)) {
$tabbox.insertAdjacentElement('beforeend', tabboxHTML);
}
}
$.one('.vc-content', that.$dom).insertAdjacentElement('beforeend', $tabbox);
});
// render top bar
plugin.trigger('addTopBar', function (btnList) {
if (!btnList) {
return;
}
let $topbar = $.one('.vc-topbar', that.$dom);
for (let i = 0; i < btnList.length; i++) {
let item = btnList[i];
let $item = $.render(tplTopBarItem, {
name: item.name || 'Undefined',
className: item.className || '',
pluginID: plugin.id
});
if (item.data) {
for (let k in item.data) {
$item.dataset[k] = item.data[k];
}
}
if (tool.isFunction(item.onClick)) {
$.bind($item, 'click', function (e) {
let enable = item.onClick.call($item);
if (enable === false) {
// do nothing
} else {
$.removeClass($.all('.vc-topbar-' + plugin.id), 'vc-actived');
$.addClass($item, 'vc-actived');
}
});
}
$topbar.insertAdjacentElement('beforeend', $item);
}
});
// render tool bar
plugin.trigger('addTool', function (toolList) {
if (!toolList) {
return;
}
let $defaultBtn = $.one('.vc-tool-last');
for (let i = 0; i < toolList.length; i++) {
let item = toolList[i];
let $item = $.render(tplToolItem, {
name: item.name || 'Undefined',
pluginID: plugin.id
});
if (item.global == true) {
$.addClass($item, 'vc-global-tool');
}
if (tool.isFunction(item.onClick)) {
$.bind($item, 'click', function (e) {
item.onClick.call($item);
});
}
$defaultBtn.parentNode.insertBefore($item, $defaultBtn);
}
});
// end init
plugin.trigger('ready');
}
/**
* trigger an event for each plugin
* @private
*/
_triggerPluginsEvent(eventName) {
for (let id in this.pluginList) {
this.pluginList[id].trigger(eventName);
}
}
/**
* trigger an event by plugin's name
* @private
*/
_triggerPluginEvent(pluginName, eventName) {
let plugin = this.pluginList[pluginName];
if (plugin) {
plugin.trigger(eventName);
}
}
/**
* add a new plugin
* @public
* @param object VConsolePlugin object
* @return boolean
*/
addPlugin(plugin) {
// ignore this plugin if it has already been installed
if (this.pluginList[plugin.id] !== undefined) {
console.warn('Plugin ' + plugin.id + ' has already been added.');
return false;
}
this.pluginList[plugin.id] = plugin;
// init plugin only if vConsole is ready
if (this.isReady) {
this._initPlugin(plugin);
// if it's the first plugin, show it by default
if (this.tabList.length == 1) {
this.showTab(this.tabList[0]);
}
}
return true;
}
/**
* remove a plugin
* @public
* @param string pluginID
* @return boolean
*/
removePlugin(pluginID) {
pluginID = (pluginID + '').toLowerCase();
let plugin = this.pluginList[pluginID];
// skip if is has not been installed
if (plugin === undefined) {
console.warn('Plugin ' + pluginID + ' does not exist.');
return false;
}
// trigger `remove` event before uninstall
plugin.trigger('remove');
// the plugin will not be initialized before vConsole is ready,
// so the plugin does not need to handle DOM-related actions in this case
if (this.isReady) {
let $tabbar = $.one('#__vc_tab_' + pluginID);
$tabbar && $tabbar.parentNode.removeChild($tabbar);
// remove topbar
let $topbar = $.all('.vc-topbar-' + pluginID, this.$dom);
for (let i = 0; i < $topbar.length; i++) {
$topbar[i].parentNode.removeChild($topbar[i]);
}
// remove content
let $content = $.one('#__vc_log_' + pluginID);
$content && $content.parentNode.removeChild($content);
// remove tool bar
let $toolbar = $.all('.vc-tool-' + pluginID, this.$dom);
for (let i = 0; i < $toolbar.length; i++) {
$toolbar[i].parentNode.removeChild($toolbar[i]);
}
}
// remove plugin from list
let index = this.tabList.indexOf(pluginID);
if (index > -1) {
this.tabList.splice(index, 1);
}
try {
delete this.pluginList[pluginID];
} catch (e) {
this.pluginList[pluginID] = undefined;
}
// show the first plugin by default
if (this.activedTab == pluginID) {
if (this.tabList.length > 0) {
this.showTab(this.tabList[0]);
}
}
return true;
}
/**
* show console panel
* @public
*/
show() {
let that = this;
// before show console panel,
// trigger a transitionstart event to make panel's property 'display' change from 'none' to 'block'
let $panel = $.one('.vc-panel', this.$dom);
$panel.style.display = 'block';
// set 10ms delay to fix confict between display and transition
setTimeout(function () {
$.addClass(that.$dom, 'vc-toggle');
that._triggerPluginsEvent('showConsole');
let $mask = $.one('.vc-mask', that.$dom);
$mask.style.display = 'block';
}, 10);
}
/**
* hide console paneldocument.body.scrollTop
* @public
*/
hide() {
$.removeClass(this.$dom, 'vc-toggle');
this._triggerPluginsEvent('hideConsole');
let $mask = $.one('.vc-mask', this.$dom),
$panel = $.one('.vc-panel', this.$dom);
$.bind($mask, 'transitionend', function (evnet) {
$mask.style.display = 'none';
$panel.style.display = 'none';
});
}
/**
* show a tab
* @public
*/
showTab(tabID) {
let $logbox = $.one('#__vc_log_' + tabID);
// set actived status
$.removeClass($.all('.vc-tab', this.$dom), 'vc-actived');
$.addClass($.one('#__vc_tab_' + tabID), 'vc-actived');
$.removeClass($.all('.vc-logbox', this.$dom), 'vc-actived');
$.addClass($logbox, 'vc-actived');
// show topbar
let $curTopbar = $.all('.vc-topbar-' + tabID, this.$dom);
$.removeClass($.all('.vc-toptab', this.$dom), 'vc-toggle');
$.addClass($curTopbar, 'vc-toggle');
if ($curTopbar.length > 0) {
$.addClass($.one('.vc-content', this.$dom), 'vc-has-topbar');
} else {
$.removeClass($.one('.vc-content', this.$dom), 'vc-has-topbar');
}
// show toolbar
$.removeClass($.all('.vc-tool', this.$dom), 'vc-toggle');
$.addClass($.all('.vc-tool-' + tabID, this.$dom), 'vc-toggle');
// trigger plugin event
this._triggerPluginEvent(this.activedTab, 'hide');
this.activedTab = tabID;
this._triggerPluginEvent(this.activedTab, 'show');
}
} // END class
export default VConsole;